ActivitySummary

This commit is contained in:
2026-05-02 19:21:13 +02:00
parent 93873b721d
commit 1bc195d1cf
28 changed files with 364 additions and 285 deletions

View File

@@ -0,0 +1,45 @@
package com.peakup.activity.dao;
import com.peakup.activity.model.ActivitySummary;
import com.peakup.activity.model.ActivitySummary_;
import com.peakup.common.dao.AbstractTimestampedDAO;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Root;
import java.time.Instant;
import java.util.List;
@ApplicationScoped
public class ActivitySummaryDAO extends AbstractTimestampedDAO<ActivitySummary> {
@Override
protected Class<ActivitySummary> getEntityClass() {
return ActivitySummary.class;
}
public List<ActivitySummary> getByAthleteId(final Long athleteId) {
final CriteriaBuilder cb = em.getCriteriaBuilder();
final CriteriaQuery<ActivitySummary> cq = cb.createQuery(ActivitySummary.class);
final Root<ActivitySummary> root = cq.from(ActivitySummary.class);
cq.select(root)
.where(cb.equal(root.get(ActivitySummary_.athleteId), athleteId))
.orderBy(cb.desc(root.get(ActivitySummary_.startTime)));
return em.createQuery(cq).getResultList();
}
public List<ActivitySummary> getByAthleteIdBetween(final Long athleteId, final Instant from, final Instant to) {
final CriteriaBuilder cb = em.getCriteriaBuilder();
final CriteriaQuery<ActivitySummary> cq = cb.createQuery(ActivitySummary.class);
final Root<ActivitySummary> root = cq.from(ActivitySummary.class);
cq.select(root)
.where(
cb.equal(root.get(ActivitySummary_.athleteId), athleteId),
cb.greaterThanOrEqualTo(root.get(ActivitySummary_.startTime), from),
cb.lessThan(root.get(ActivitySummary_.startTime), to)
)
.orderBy(cb.desc(root.get(ActivitySummary_.startTime)));
return em.createQuery(cq).getResultList();
}
}

View File

@@ -1,48 +0,0 @@
package com.peakup.activity.mapper;
import com.peakup.activity.model.ActivitySummary;
import com.peakup.activity.model.IntervalsActivity;
import jakarta.annotation.Nullable;
import org.mapstruct.AfterMapping;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.MappingConstants;
import org.mapstruct.MappingTarget;
import org.mapstruct.ReportingPolicy;
import java.util.HashMap;
import java.util.Map;
@SuppressWarnings("NullableProblems")
@Mapper(
componentModel = MappingConstants.ComponentModel.CDI,
unmappedTargetPolicy = ReportingPolicy.ERROR,
unmappedSourcePolicy = ReportingPolicy.IGNORE
)
public interface ActivitySummaryMapper {
private static void putIfNonZero(final Map<String, String> map, final String key, final @Nullable Double value) {
if (value != null && value != 0.0) {
map.put(key, String.valueOf(value));
}
}
@Mapping(target = "activityId", expression = "java(source.getId() != null ? source.getId() : 0L)")
@Mapping(target = "valuesForSummary", ignore = true)
ActivitySummary toSummary(IntervalsActivity source);
@AfterMapping
default void populateValuesForSummary(@MappingTarget final ActivitySummary target, final IntervalsActivity source) {
final Map<String, String> values = new HashMap<>();
putIfNonZero(values, "distanceM", source.getDistanceM());
putIfNonZero(values, "averageHeartrateBpm", source.getAverageHeartrateBpm());
putIfNonZero(values, "averagePowerWatt", source.getAveragePowerWatt());
putIfNonZero(values, "normalizedPowerWatt", source.getNormalizedPowerWatt());
putIfNonZero(values, "trainingLoad", source.getTrainingLoad());
putIfNonZero(values, "ctl", source.getCtl());
putIfNonZero(values, "atl", source.getAtl());
putIfNonZero(values, "averageSpeedMps", source.getAverageSpeedMps());
putIfNonZero(values, "paceMps", source.getPaceMps());
target.setValuesForSummary(values);
}
}

View File

@@ -1,2 +0,0 @@
@com.peakup.common.annotation.NonNull
package com.peakup.activity.mapper;

View File

@@ -17,28 +17,28 @@ import java.time.Instant;
@EqualsAndHashCode(callSuper = true)
public abstract class AbstractActivity extends AbstractTimestampedEntity {
@Column
@Column(name = "athlete_id")
private Long athleteId;
@Column
@Column(name = "origin_id")
public String originId;
@Column
@Column(name = "name")
public String name;
@Column
@Column(name = "type")
public String type;
@Column
@Column(name = "sport_type")
public String sportType;
@Column
@Column(name = "activity_type")
public ActivityTypeEnum activityType;
@Column
@Column(name = "start_time")
public Instant startTime;
@Column
@Column(name = "timezone")
public String timezone;
@Column(name = "distance_m")
@@ -68,22 +68,22 @@ public abstract class AbstractActivity extends AbstractTimestampedEntity {
@Column(name = "average_cadence_rpm")
public Double averageCadenceRPM;
@Column
@Column(name = "average_power_watt")
public Double averagePowerWatt;
@Column
@Column(name = "max_power_watt")
public Double maxPowerWatt;
@Column
@Column(name = "normalized_power_watt")
public Double normalizedPowerWatt;
@Column(name = "total_work_kj")
public Double totalWorkKJ;
@Column
@Column(name = "calories")
public Double calories;
@Column
@Column(name = "linked_workout_id")
public @Nullable Long linkedWorkoutId;
@SuppressWarnings("NullAway.Init")

View File

@@ -12,25 +12,25 @@ public class ActivityAchievement {
@Column(name = "achievement_id")
public @Nullable String achievementId;
@Column
@Column(name = "type")
public @Nullable String type;
@Column
@Column(name = "message")
public @Nullable String message;
@Column
@Column(name = "watts")
public @Nullable Integer watts;
@Column
@Column(name = "secs")
public @Nullable Integer secs;
@Column
@Column(name = "value")
public @Nullable Integer value;
@Column
@Column(name = "distance")
public @Nullable Double distance;
@Column
@Column(name = "pace")
public @Nullable Double pace;
@SuppressWarnings("NullAway.Init")

View File

@@ -12,13 +12,13 @@ public class ActivityAttachment {
@Column(name = "attachment_id")
public @Nullable String attachmentId;
@Column
@Column(name = "filename")
public @Nullable String filename;
@Column
@Column(name = "mimetype")
public @Nullable String mimetype;
@Column
@Column(name = "url")
public @Nullable String url;
@SuppressWarnings("NullAway.Init")

View File

@@ -22,13 +22,13 @@ import java.time.Instant;
public class ActivityMapStreamPoint extends AbstractStream implements DependentEntity {
@Id
@Column(nullable = false)
@Column(name = "activity_id", nullable = false)
private long activityId;
@Column(nullable = false)
@Column(name = "lat", nullable = false)
private double lat;
@Column(nullable = false)
@Column(name = "lng", nullable = false)
private double lng;
@SuppressWarnings("NullAway.Init")

View File

@@ -23,7 +23,7 @@ import java.time.Instant;
public class ActivityStreamPoint extends AbstractStream implements DependentEntity {
@Id
@Column(nullable = false)
@Column(name = "activity_id", nullable = false)
private long activityId;
@Column(name = "elapsed_s", nullable = false)
@@ -38,7 +38,7 @@ public class ActivityStreamPoint extends AbstractStream implements DependentEnti
@Column(name = "heartrate_bpm")
private @Nullable Integer heartrateBpm;
@Column
@Column(name = "watts")
private @Nullable Integer watts;
@Column(name = "torque_nm")
@@ -50,10 +50,10 @@ public class ActivityStreamPoint extends AbstractStream implements DependentEnti
@Column(name = "velocity_mps")
private @Nullable Double velocityMps;
@Column
@Column(name = "lat")
private @Nullable Double lat;
@Column
@Column(name = "lng")
private @Nullable Double lng;
@Column(name = "temp_c")
@@ -62,7 +62,7 @@ public class ActivityStreamPoint extends AbstractStream implements DependentEnti
@Column(name = "grade_smooth")
private @Nullable Double gradeSmooth;
@Column
@Column(name = "moving")
private @Nullable Boolean moving;
@Column(name = "lr_balance")

View File

@@ -1,28 +1,52 @@
package com.peakup.activity.model;
import com.peakup.common.model.AbstractDataObject;
import lombok.AllArgsConstructor;
import com.peakup.common.model.AbstractTimestampedEntity;
import jakarta.annotation.Nullable;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Transient;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.hibernate.annotations.Immutable;
import org.hibernate.annotations.Subselect;
import org.hibernate.annotations.Synchronize;
import java.time.Instant;
import java.util.Map;
@Entity
@Immutable
@Subselect(ActivitySummary.QUERY)
@Synchronize("intervals_activity")
@Data
@AllArgsConstructor
@EqualsAndHashCode(callSuper = false)
public class ActivitySummary extends AbstractDataObject {
@EqualsAndHashCode(callSuper = true)
public class ActivitySummary extends AbstractTimestampedEntity {
public ActivityTypeEnum activityType;
public Instant startTime;
public Long movingTimeS;
public Double calories;
private long activityId;
public static final String QUERY = "SELECT ID, athlete_id, activity_type, start_time, moving_time_s, calories, name, modified_at, created_at FROM intervals_activity";
@Column(name = "athlete_id")
private Long athleteId;
@Column(name = "activity_type")
private ActivityTypeEnum activityType;
@Column(name = "start_time")
private Instant startTime;
@Column(name = "moving_time_s")
private Long movingTimeS;
@Column(name = "calories")
private Double calories;
@Column(name = "name")
private String name;
private Map<String, String> valuesForSummary;
@Transient
private @Nullable Map<String, String> valuesForSummary;
@SuppressWarnings("NullAway.Init")
public ActivitySummary() {
// hibernate
}
}

View File

@@ -26,7 +26,7 @@ public class ActivityZone {
@Column(name = "value_end")
public double valueEnd;
@Column
@Column(name = "secs")
public int secs;
@SuppressWarnings("NullAway.Init")

View File

@@ -15,13 +15,13 @@ public class IgnorePart {
@Column(name = "end_index")
public int endIndex;
@Column
@Column(name = "power")
public @Nullable Boolean power;
@Column
@Column(name = "pace")
public @Nullable Boolean pace;
@Column
@Column(name = "hr")
public @Nullable Boolean hr;
@SuppressWarnings("NullAway.Init")

View File

@@ -24,31 +24,31 @@ import java.util.List;
@EqualsAndHashCode(callSuper = true)
public class IntervalsActivity extends AbstractActivity {
@Column
@Column(name = "strava_origin_id")
public String stravaOriginId;
@Column(columnDefinition = "TEXT")
@Column(name = "description", columnDefinition = "TEXT")
public @Nullable String description;
@Column
@Column(name = "trainer")
public @Nullable Boolean trainer;
@Column
@Column(name = "commute")
public @Nullable Boolean commute;
@Column
@Column(name = "race")
public @Nullable Boolean race;
@Column
@Column(name = "gear_id")
public String gearId;
@Column
@Column(name = "gear_name")
public @Nullable String gearName;
@Column(name = "gear_distance_m")
public @Nullable Double gearDistanceM;
@Column
@Column(name = "gear_primary")
public @Nullable Boolean gearPrimary;
@Column(name = "total_ascent_m")
@@ -63,31 +63,31 @@ public class IntervalsActivity extends AbstractActivity {
@Column(name = "max_altitude_m")
public Double maxAltitudeM;
@Column
@Column(name = "training_load")
public Double trainingLoad;
@Column
@Column(name = "intensity_factor")
public Double intensityFactor;
@Column
@Column(name = "efficiency_factor")
public Double efficiencyFactor;
@Column
@Column(name = "ftp")
public @Nullable Integer ftp;
@Column
@Column(name = "atl")
public Double atl;
@Column
@Column(name = "ctl")
public Double ctl;
@Column
@Column(name = "hrr_bpm_drop")
public @Nullable Double hrrBpmDrop;
@Column
@Column(name = "hrr_start_bpm")
public @Nullable Integer hrrStartBpm;
@Column
@Column(name = "hrr_end_bpm")
public @Nullable Integer hrrEndBpm;
@Column(name = "hrr_start_time_s")
@@ -96,16 +96,16 @@ public class IntervalsActivity extends AbstractActivity {
@Column(name = "hrr_end_time_s")
public @Nullable Integer hrrEndTimeSec;
@Column
@Column(name = "hrr_average_watts")
public @Nullable Integer hrrAverageWatts;
@Column
@Column(name = "variability_index")
public Double variabilityIndex;
@Column
@Column(name = "feel")
public @Nullable Integer feel;
@Column
@Column(name = "perceived_exertion")
public Double perceivedExertion;
@Column(name = "average_stride_length_m")
@@ -114,7 +114,7 @@ public class IntervalsActivity extends AbstractActivity {
@Column(name = "average_temp_c")
public Double averageTempC;
@Column(columnDefinition = "integer[]")
@Column(name = "recording_stops", columnDefinition = "integer[]")
@JdbcTypeCode(SqlTypes.ARRAY)
public @Nullable int[] recordingStops;
@@ -153,39 +153,39 @@ public class IntervalsActivity extends AbstractActivity {
@Column(name = "max_temp_c")
public @Nullable Double maxTempC;
@Column
@Column(name = "avg_lr_balance")
public @Nullable Double avgLrBalance;
// --- Grade Adjusted Pace ---
@Column(name = "gap_mps")
public @Nullable Double gapMps;
@Column
@Column(name = "gap_model")
public @Nullable String gapModel;
// --- Device / equipment ---
@Column
@Column(name = "device_name")
public @Nullable String deviceName;
@Column
@Column(name = "device_watts")
public @Nullable Boolean deviceWatts;
@Column
@Column(name = "power_meter")
public @Nullable String powerMeter;
@Column
@Column(name = "power_meter_serial")
public @Nullable String powerMeterSerial;
@Column
@Column(name = "power_meter_battery")
public @Nullable String powerMeterBattery;
@Column(name = "crank_length_mm")
public @Nullable Double crankLengthMm;
@Column
@Column(name = "power_field")
public @Nullable String powerField;
@Column
@Column(name = "use_elevation_correction")
public @Nullable Boolean useElevationCorrection;
// --- Nutrition ---
@@ -197,7 +197,7 @@ public class IntervalsActivity extends AbstractActivity {
public @Nullable Double icuDistanceM;
// --- Time accounting ---
@Column
@Column(name = "icu_ignore_time")
public @Nullable Boolean icuIgnoreTime;
@Column(name = "icu_recording_time_s")
@@ -213,35 +213,35 @@ public class IntervalsActivity extends AbstractActivity {
public @Nullable Integer icuCooldownTimeS;
// --- Activity sub-type and pairing ---
@Column
@Column(name = "sub_type")
public @Nullable String subType;
@Column
@Column(name = "paired_event_id")
public @Nullable String pairedEventId;
// --- Athlete state at activity time ---
@Column(name = "icu_weight_kg")
public @Nullable Double icuWeightKg;
@Column
@Column(name = "lthr")
public @Nullable Integer lthr;
@Column
@Column(name = "icu_resting_hr")
public @Nullable Integer icuRestingHr;
@Column(name = "threshold_pace_mps")
public @Nullable Double thresholdPaceMps;
@Column
@Column(name = "icu_power_spike_threshold")
public @Nullable Integer icuPowerSpikeThreshold;
@Column
@Column(name = "icu_sweet_spot_min")
public @Nullable Double icuSweetSpotMin;
@Column
@Column(name = "icu_sweet_spot_max")
public @Nullable Double icuSweetSpotMax;
@Column
@Column(name = "trimp")
public @Nullable Double trimp;
// --- W' and P-max ---
@@ -252,7 +252,7 @@ public class IntervalsActivity extends AbstractActivity {
public @Nullable Integer pMaxWatt;
// --- Performance model (PM) ---
@Column
@Column(name = "icu_pm_cp")
public @Nullable Integer icuPmCp;
@Column(name = "icu_pm_w_prime_j")
@@ -261,17 +261,17 @@ public class IntervalsActivity extends AbstractActivity {
@Column(name = "icu_pm_p_max_watt")
public @Nullable Integer icuPmPMaxWatt;
@Column
@Column(name = "icu_pm_ftp")
public @Nullable Integer icuPmFtp;
@Column
@Column(name = "icu_pm_ftp_secs")
public @Nullable Integer icuPmFtpSecs;
@Column
@Column(name = "icu_pm_ftp_watts")
public @Nullable Integer icuPmFtpWatts;
// --- Rolling performance ---
@Column
@Column(name = "icu_rolling_cp")
public @Nullable Integer icuRollingCp;
@Column(name = "icu_rolling_w_prime_j")
@@ -280,14 +280,14 @@ public class IntervalsActivity extends AbstractActivity {
@Column(name = "icu_rolling_p_max_watt")
public @Nullable Integer icuRollingPMaxWatt;
@Column
@Column(name = "icu_rolling_ftp")
public @Nullable Integer icuRollingFtp;
@Column
@Column(name = "icu_rolling_ftp_delta")
public @Nullable Integer icuRollingFtpDelta;
// --- Sweet-spot CP model ---
@Column
@Column(name = "ss_cp")
public @Nullable Integer ssCp;
@Column(name = "ss_w_prime_j")
@@ -297,176 +297,176 @@ public class IntervalsActivity extends AbstractActivity {
public @Nullable Integer ssPMaxWatt;
// --- Ignore flags ---
@Column
@Column(name = "icu_ignore_power")
public @Nullable Boolean icuIgnorePower;
@Column
@Column(name = "icu_ignore_hr")
public @Nullable Boolean icuIgnoreHr;
@Column
@Column(name = "ignore_velocity")
public @Nullable Boolean ignoreVelocity;
@Column
@Column(name = "ignore_pace")
public @Nullable Boolean ignorePace;
// --- Activity flags ---
@Column
@Column(name = "has_heartrate")
public @Nullable Boolean hasHeartrate;
@Column
@Column(name = "analyzed")
public @Nullable String analyzed;
@Column
@Column(name = "has_weather")
public @Nullable Boolean hasWeather;
@Column
@Column(name = "has_segments")
public @Nullable Boolean hasSegments;
// --- File / sync metadata ---
@Column
@Column(name = "file_type")
public @Nullable String fileType;
@Column
@Column(name = "file_sport_index")
public @Nullable Integer fileSportIndex;
@Column
@Column(name = "icu_athlete_id")
public @Nullable String icuAthleteId;
@Column
@Column(name = "icu_created")
public @Nullable String icuCreated;
@Column
@Column(name = "icu_sync_date")
public @Nullable String icuSyncDate;
@Column
@Column(name = "icu_chat_id")
public @Nullable Long icuChatId;
// --- Local-timezone start ---
@Column
@Column(name = "start_date_local")
public @Nullable String startDateLocal;
// --- Zone definitions (JSON) ---
@Column(columnDefinition = "TEXT")
@Column(name = "icu_hr_zones", columnDefinition = "TEXT")
public @Nullable String icuHrZones;
@Column(columnDefinition = "TEXT")
@Column(name = "pace_zones", columnDefinition = "TEXT")
public @Nullable String paceZones;
@Column(columnDefinition = "TEXT")
@Column(name = "icu_power_zones", columnDefinition = "TEXT")
public @Nullable String icuPowerZones;
// --- Complex nested data (JSON) ---
@Column(columnDefinition = "TEXT")
@Column(name = "icu_training_load_data", columnDefinition = "TEXT")
public @Nullable String icuTrainingLoadData;
@Column(columnDefinition = "TEXT")
@Column(name = "interval_summary", columnDefinition = "TEXT")
public @Nullable String intervalSummary;
// --- Available stream names ---
@Column(columnDefinition = "text[]")
@Column(name = "stream_types", columnDefinition = "text[]")
@JdbcTypeCode(SqlTypes.ARRAY)
public @Nullable String[] streamTypes;
// --- Skyline chart ---
@Column(columnDefinition = "TEXT")
@Column(name = "skyline_chart_bytes", columnDefinition = "TEXT")
public @Nullable String skylineChartBytes;
// --- Zone times (JSON) ---
@Column(columnDefinition = "TEXT")
@Column(name = "icu_zone_times", columnDefinition = "TEXT")
public @Nullable String icuZoneTimes;
@Column(columnDefinition = "TEXT")
@Column(name = "icu_hr_zone_times", columnDefinition = "TEXT")
public @Nullable String icuHrZoneTimes;
@Column(columnDefinition = "TEXT")
@Column(name = "pace_zone_times", columnDefinition = "TEXT")
public @Nullable String paceZoneTimes;
@Column(columnDefinition = "TEXT")
@Column(name = "gap_zone_times", columnDefinition = "TEXT")
public @Nullable String gapZoneTimes;
@Column
@Column(name = "use_gap_zone_times")
public @Nullable Boolean useGapZoneTimes;
@Column
@Column(name = "tiz_order")
public @Nullable String tizOrder;
// --- Performance / load metrics ---
@Column
@Column(name = "polarization_index")
public @Nullable Double polarizationIndex;
@Column
@Column(name = "decoupling")
public @Nullable Double decoupling;
@Column
@Column(name = "icu_lap_count")
public @Nullable Integer icuLapCount;
@Column
@Column(name = "icu_joules_above_ftp")
public @Nullable Integer icuJoulesAboveFtp;
@Column
@Column(name = "icu_max_wbal_depletion")
public @Nullable Integer icuMaxWbalDepletion;
@Column
@Column(name = "icu_power_hr_z2")
public @Nullable Double icuPowerHrZ2;
@Column
@Column(name = "icu_power_hr_z2_mins")
public @Nullable Integer icuPowerHrZ2Mins;
@Column
@Column(name = "icu_cadence_z2")
public @Nullable Double icuCadenceZ2;
@Column
@Column(name = "icu_rpe")
public @Nullable Double icuRpe;
@Column
@Column(name = "icu_power_hr")
public @Nullable Double icuPowerHr;
@Column
@Column(name = "icu_median_time_delta")
public @Nullable Integer icuMedianTimeDelta;
@Column
@Column(name = "p30s_exponent")
public @Nullable Double p30sExponent;
// --- Intervals editing ---
@Column
@Column(name = "icu_intervals_edited")
public @Nullable Boolean icuIntervalsEdited;
@Column
@Column(name = "lock_intervals")
public @Nullable Boolean lockIntervals;
@Column
@Column(name = "workout_shift_secs")
public @Nullable Integer workoutShiftSecs;
// --- Swimming ---
@Column(name = "pool_length_m")
public @Nullable Integer poolLengthM;
@Column(columnDefinition = "TEXT")
@Column(name = "lengths", columnDefinition = "TEXT")
public @Nullable String lengths;
// --- Load scores ---
@Column
@Column(name = "power_load")
public @Nullable Double powerLoad;
@Column
@Column(name = "hr_load")
public @Nullable Double hrLoad;
@Column
@Column(name = "pace_load")
public @Nullable Double paceLoad;
@Column
@Column(name = "hr_load_type")
public @Nullable String hrLoadType;
@Column
@Column(name = "pace_load_type")
public @Nullable String paceLoadType;
@Column
@Column(name = "session_rpe")
public @Nullable Double sessionRpe;
@Column
@Column(name = "strain_score")
public @Nullable Double strainScore;
@Column
@Column(name = "kg_lifted")
public @Nullable Double kgLifted;
// --- Weather ---
@@ -497,16 +497,16 @@ public class IntervalsActivity extends AbstractActivity {
@Column(name = "average_wind_gust_mps")
public @Nullable Double averageWindGustMps;
@Column
@Column(name = "prevailing_wind_deg")
public @Nullable Double prevailingWindDeg;
@Column
@Column(name = "headwind_percent")
public @Nullable Double headwindPercent;
@Column
@Column(name = "tailwind_percent")
public @Nullable Double tailwindPercent;
@Column
@Column(name = "average_clouds")
public @Nullable Double averageClouds;
@Column(name = "max_rain_mm")
@@ -524,50 +524,50 @@ public class IntervalsActivity extends AbstractActivity {
public @Nullable Double paceMps;
// --- Athlete / HR ---
@Column
@Column(name = "athlete_max_hr")
public @Nullable Integer athleteMaxHr;
// --- Coach / compliance ---
@Column
@Column(name = "compliance")
public @Nullable String compliance;
@Column
@Column(name = "coach_tick")
public @Nullable Boolean coachTick;
@Column(name = "activity_group")
public @Nullable String activityGroup;
// --- Source / sync metadata ---
@Column(columnDefinition = "TEXT")
@Column(name = "icu_sync_error", columnDefinition = "TEXT")
public @Nullable String icuSyncError;
@Column
@Column(name = "icu_color")
public @Nullable String icuColor;
@Column
@Column(name = "source")
public @Nullable String source;
@Column
@Column(name = "oauth_client_id")
public @Nullable String oauthClientId;
@Column
@Column(name = "oauth_client_name")
public @Nullable String oauthClientName;
@Column
@Column(name = "strava_id")
public @Nullable Long stravaId;
@Column
@Column(name = "route_id")
public @Nullable Long routeId;
// --- Power field names ---
@Column(columnDefinition = "TEXT")
@Column(name = "power_field_names", columnDefinition = "TEXT")
public @Nullable String powerFieldNames;
// --- Stream import status ---
@Column
@Column(name = "stream_status")
public StreamStatusEnum streamStatus = StreamStatusEnum.PENDING;
@Column(columnDefinition = "TEXT")
@Column(name = "stream_import_error", columnDefinition = "TEXT")
public @Nullable String streamImportError;
@SuppressWarnings("NullAway.Init")

View File

@@ -13,7 +13,7 @@ import lombok.EqualsAndHashCode;
@EqualsAndHashCode(callSuper = true)
public class StravaActivity extends AbstractActivity {
@Column
@Column(name = "device_name")
public String deviceName;
@Convert(converter = IntervalsImportStatusEnum.Converter.class)

View File

@@ -0,0 +1,38 @@
package com.peakup.activity.service;
import com.peakup.activity.dao.ActivitySummaryDAO;
import com.peakup.activity.model.ActivitySummary;
import com.peakup.common.service.AbstractService;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneOffset;
import java.util.List;
@ApplicationScoped
public class ActivitySummaryService extends AbstractService<ActivitySummary, ActivitySummaryDAO> {
@Inject
@SuppressWarnings("NullAway.Init")
ActivitySummaryDAO activitySummaryDAO;
@Override
protected ActivitySummaryDAO getDAO() {
return activitySummaryDAO;
}
@Transactional
public List<ActivitySummary> getByAthleteId(final Long athleteId) {
return activitySummaryDAO.getByAthleteId(athleteId);
}
@Transactional
public List<ActivitySummary> getByAthleteIdBetween(final Long athleteId, final String from, final String to) {
final Instant fromInstant = LocalDate.parse(from).atStartOfDay(ZoneOffset.UTC).toInstant();
final Instant toInstant = LocalDate.parse(to).plusDays(1).atStartOfDay(ZoneOffset.UTC).toInstant();
return activitySummaryDAO.getByAthleteIdBetween(athleteId, fromInstant, toInstant);
}
}

View File

@@ -15,19 +15,19 @@ import lombok.EqualsAndHashCode;
@AllArgsConstructor
public class Athlete extends AbstractTimestampedEntity {
@Column(nullable = false)
@Column(name = "first_name", nullable = false)
private String firstName;
@Column(nullable = false)
@Column(name = "last_name", nullable = false)
private String lastName;
@Column(nullable = false)
@Column(name = "login", nullable = false)
private String login;
@Column(nullable = false)
@Column(name = "email", nullable = false)
private String email;
@Column(nullable = false)
@Column(name = "weight", nullable = false)
private Double weight;
@Convert(converter = SexEnum.Converter.class)

View File

@@ -13,7 +13,7 @@ import java.time.Instant;
public abstract class AbstractTimescaleEntity {
@Id
@Column(nullable = false)
@Column(name = "time", nullable = false)
private Instant time;
protected AbstractTimescaleEntity() {

View File

@@ -18,10 +18,10 @@ import java.time.Instant;
@AllArgsConstructor
public abstract class AbstractTimestampedEntity extends AbstractEntity {
@Column(nullable = false)
@Column(name = "modified_at", nullable = false)
private Instant modifiedAt = Instant.now();
@Column(nullable = false)
@Column(name = "created_at", nullable = false)
private Instant createdAt = Instant.now();
@PrePersist

View File

@@ -14,13 +14,13 @@ import lombok.EqualsAndHashCode;
@SuppressWarnings("NullAway.Init")
public class IntervalsConnector extends AbstractTimestampedEntity {
@Column(nullable = false)
@Column(name = "athlete_id", nullable = false)
private Long athleteId; // loose coupling to Athlete
@Column(nullable = false)
@Column(name = "intervals_athlete_id", nullable = false)
private String intervalsAthleteId; // e.g. "i_1234567"
@Column(nullable = false)
@Column(name = "api_key", nullable = false)
private String apiKey;
@SuppressWarnings("NullAway.Init")

View File

@@ -14,25 +14,25 @@ import lombok.EqualsAndHashCode;
@SuppressWarnings("NullAway.Init")
public class StravaConnector extends AbstractTimestampedEntity {
@Column(nullable = false)
@Column(name = "athlete_id", nullable = false)
private Long athleteId; // loose coupling
@Column(nullable = false)
@Column(name = "strava_athlete_id", nullable = false)
private String stravaAthleteId;
@Column(nullable = false)
@Column(name = "access_token", nullable = false)
private String accessToken;
@Column(nullable = false)
@Column(name = "refresh_token", nullable = false)
private String refreshToken;
@Column(nullable = false)
@Column(name = "expires_at", nullable = false)
private long expiresAt;
@Column(nullable = false)
@Column(name = "client_id", nullable = false)
private String clientId;
@Column(nullable = false)
@Column(name = "client_secret", nullable = false)
private String clientSecret;
@SuppressWarnings("NullAway.Init")

View File

@@ -1,13 +1,11 @@
package com.peakup.dashboard.service;
import com.peakup.activity.mapper.ActivitySummaryMapper;
import com.peakup.activity.model.ActivityMapStreamPoint;
import com.peakup.activity.model.ActivityStreamPoint;
import com.peakup.activity.model.ActivitySummary;
import com.peakup.activity.model.IntervalsActivity;
import com.peakup.activity.service.ActivityMapStreamService;
import com.peakup.activity.service.ActivityStreamService;
import com.peakup.activity.service.IntervalsActivityService;
import com.peakup.activity.service.ActivitySummaryService;
import com.peakup.dashboard.model.Dashboard;
import com.peakup.goal.service.GoalMetricPeriodTotalService;
import com.peakup.goal.service.GoalService;
@@ -30,7 +28,7 @@ public class DashboardService {
@Inject
@SuppressWarnings("NullAway.Init")
IntervalsActivityService intervalsActivityService;
ActivitySummaryService activitySummaryService;
@Inject
@SuppressWarnings("NullAway.Init")
@@ -48,10 +46,6 @@ public class DashboardService {
@SuppressWarnings("NullAway.Init")
GoalMetricPeriodTotalService goalMetricPeriodTotalService;
@Inject
@SuppressWarnings("NullAway.Init")
ActivitySummaryMapper activitySummaryMapper;
@Transactional
public Dashboard getDashboard(final long athleteId, final int calendarWeek, final int year) {
final LocalDate selectedWeekMonday = LocalDate.now()
@@ -65,14 +59,11 @@ public class DashboardService {
final String from = rangeFrom.format(DateTimeFormatter.ISO_LOCAL_DATE);
final String to = rangeTo.format(DateTimeFormatter.ISO_LOCAL_DATE);
final List<IntervalsActivity> activities = intervalsActivityService.getByAthleteIdBetween(athleteId, from, to);
final List<ActivitySummary> activitySummaries = activitySummaryService.getByAthleteIdBetween(athleteId, from, to);
final Map<Long, List<ActivityMapStreamPoint>> weekActivityMapStreams =
resolveWeekActivityMapStreams(activities, selectedWeekMonday);
resolveWeekActivityMapStreams(activitySummaries, selectedWeekMonday);
final Map<Long, List<ActivityStreamPoint>> weekActivityStreams =
resolveWeekActivityStreams(activities, selectedWeekMonday);
final List<ActivitySummary> activitySummaries = activities.stream()
.map(activitySummaryMapper::toSummary)
.toList();
resolveWeekActivityStreams(activitySummaries, selectedWeekMonday);
return new Dashboard(
calendarWeek,
@@ -85,7 +76,7 @@ public class DashboardService {
);
}
private List<IntervalsActivity> filterToWeek(final List<IntervalsActivity> activities, final LocalDate weekMonday) {
private List<ActivitySummary> filterToWeek(final List<ActivitySummary> activities, final LocalDate weekMonday) {
final LocalDate weekSunday = weekMonday.plusDays(6);
return activities.stream()
.filter(activity -> {
@@ -97,7 +88,7 @@ public class DashboardService {
}
private Map<Long, List<ActivityMapStreamPoint>> resolveWeekActivityMapStreams(
final List<IntervalsActivity> activities,
final List<ActivitySummary> activities,
final LocalDate weekMonday) {
return filterToWeek(activities, weekMonday).stream()
.collect(Collectors.toMap(
@@ -108,7 +99,7 @@ public class DashboardService {
}
private Map<Long, List<ActivityStreamPoint>> resolveWeekActivityStreams(
final List<IntervalsActivity> activities,
final List<ActivitySummary> activities,
final LocalDate weekMonday) {
return filterToWeek(activities, weekMonday).stream()
.collect(Collectors.toMap(

View File

@@ -13,16 +13,16 @@ import lombok.EqualsAndHashCode;
@EqualsAndHashCode(callSuper = true)
public class Goal extends AbstractTimestampedEntity {
@Column(nullable = false)
@Column(name = "athlete_id", nullable = false)
public Long athleteId;
@Column(nullable = false)
@Column(name = "period", nullable = false)
public GoalPeriodEnum period;
@Column(nullable = false)
@Column(name = "metric", nullable = false)
public GoalMetricEnum metric;
@Column(nullable = false)
@Column(name = "target_value", nullable = false)
public Double targetValue;
@SuppressWarnings("NullAway.Init")

View File

@@ -18,10 +18,10 @@ public class GoalMetricPeriodTotal extends AbstractTimestampedEntity {
@Column(name = "athlete_id", nullable = false)
private Long athleteId;
@Column(nullable = false)
@Column(name = "metric", nullable = false)
private GoalMetricEnum metric;
@Column(nullable = false)
@Column(name = "period", nullable = false)
private GoalPeriodEnum period;
@Column(name = "year", nullable = false)
@@ -30,7 +30,7 @@ public class GoalMetricPeriodTotal extends AbstractTimestampedEntity {
@Column(name = "calendar_week", nullable = false)
private int calendarWeek;
@Column(nullable = false)
@Column(name = "aggregated_value", nullable = false)
private double aggregatedValue;
@SuppressWarnings("NullAway.Init")

View File

@@ -21,19 +21,19 @@ import java.util.List;
@JsonInclude(JsonInclude.Include.NON_NULL)
public class WeekPlan extends AbstractTimestampedEntity {
@Column(nullable = false)
@Column(name = "athlete_id", nullable = false)
public Long athleteId;
@Column(nullable = false)
@Column(name = "year", nullable = false)
public int year;
@Column(nullable = false)
@Column(name = "calendar_week", nullable = false)
public int calendarWeek;
@Column(nullable = false)
@Column(name = "week_label", nullable = false)
public String weekLabel;
@Column(columnDefinition = "TEXT", nullable = false)
@Column(name = "week_description", columnDefinition = "TEXT", nullable = false)
public String weekDescription;
@Transient

View File

@@ -29,23 +29,23 @@ public class Workout extends AbstractEntity implements DependentEntity {
@Column(name = "week_plan_id", nullable = false)
public long weekPlanId;
@Column(nullable = false)
@Column(name = "title", nullable = false)
public String title;
@Column(columnDefinition = "TEXT", nullable = false)
@Column(name = "description", columnDefinition = "TEXT", nullable = false)
public String description = "";
@Column(nullable = false)
@Column(name = "workout_at", nullable = false)
public Instant workoutAt;
@Column(nullable = false)
@Column(name = "workout_type", nullable = false)
public WorkoutTypeEnum workoutType;
@Column(nullable = false)
@Column(name = "sort_order", nullable = false)
public int sortOrder;
@Column(nullable = false)
@Column(name = "status", nullable = false)
public WorkoutStatusEnum status = WorkoutStatusEnum.PLANNED;
@Column(columnDefinition = "TEXT")
@Column(name = "status_note", columnDefinition = "TEXT")
public @Nullable String statusNote;
@Column(nullable = false)
@Column(name = "estimated_time_seconds", nullable = false)
public int estimatedTimeSeconds;
@Column
@Column(name = "estimated_tss")
public @Nullable Integer estimatedTss;
@OneToMany(fetch = FetchType.EAGER)
@JoinColumn(name = "workout_id")

View File

@@ -17,15 +17,15 @@ public class WorkoutLoad extends AbstractEntity implements DependentEntity {
@Column(name = "load_id", nullable = false)
public long stepId;
@Column(nullable = false)
@Column(name = "sort_index", nullable = false)
public int sortIndex;
@Column(nullable = false)
@Column(name = "step_type", nullable = false)
public WorkoutStepTypeEnum stepType;
@Column(nullable = false)
@Column(name = "duration_seconds", nullable = false)
public int durationSeconds;
@Column
@Column(name = "target_value_pct_start")
public @Nullable Integer targetValuePctStart;
@Column
@Column(name = "target_value_pct_end")
public @Nullable Integer targetValuePctEnd;
@SuppressWarnings("NullAway.Init")

View File

@@ -25,15 +25,15 @@ public class WorkoutStep extends AbstractEntity implements DependentEntity {
@Column(name = "workout_id", nullable = false)
public long workoutId;
@Column(nullable = false)
@Column(name = "sort_index", nullable = false)
public int sortIndex;
@Column(nullable = false)
@Column(name = "label", nullable = false)
public String label;
@Column(columnDefinition = "TEXT", nullable = false)
@Column(name = "description", columnDefinition = "TEXT", nullable = false)
public String description = "";
@Column(nullable = false)
@Column(name = "intensity_reference", nullable = false)
public IntensityReferenceEnum intensityReference;
@Column(nullable = false)
@Column(name = "repeat", nullable = false)
public int repeat;
@OneToMany(fetch = FetchType.EAGER)
@JoinColumn(name = "load_id")

View File

@@ -1,11 +1,17 @@
package com.peakup;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.domain.JavaField;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.core.importer.ImportOption;
import com.tngtech.archunit.lang.ArchCondition;
import com.tngtech.archunit.lang.ConditionEvents;
import com.tngtech.archunit.lang.SimpleConditionEvent;
import jakarta.persistence.Column;
import org.junit.jupiter.api.Test;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.fields;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.methods;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
@@ -128,6 +134,32 @@ class ArchitectureTest {
.check(CLASSES);
}
/**
* Every field annotated with {@code @Column} must declare an explicit {@code name}.
* Prevents silent breakage when Hibernate's naming strategy differs from expectations.
*/
@Test
void columnFieldsHaveExplicitName() {
fields().that().areAnnotatedWith(Column.class)
.should(new ArchCondition<>("have an explicit column name") {
@Override
public void check(final JavaField field, final ConditionEvents events) {
final Column column = field.getAnnotationOfType(Column.class);
if (column.name().isEmpty()) {
final String snake = field.getName()
.replaceAll("([A-Z])", "_$1").toLowerCase(java.util.Locale.ROOT);
events.add(SimpleConditionEvent.violated(
field,
field.getOwner().getSimpleName() + "." + field.getName()
+ " fix: @Column(name=\"" + snake + "\")"
));
}
}
})
.because("add name=\"snake_case\" to every @Column — prevents Hibernate naming-strategy surprises")
.check(CLASSES);
}
/**
* {@code @Transactional} is only permitted in service and importer classes.
* Resources must delegate to services; base abstract classes must not carry it.

View File

@@ -1,15 +1,14 @@
// GENERATED — do not edit manually.
import type { AbstractDataObjectDo } from './AbstractDataObjectDo';
import type { AbstractTimestampedEntityDo } from './AbstractTimestampedEntityDo';
import type { ActivityTypeEnum } from './ActivityTypeEnum';
export interface ActivitySummaryDo extends AbstractDataObjectDo {
export interface ActivitySummaryDo extends AbstractTimestampedEntityDo {
athleteId: number;
activityType: ActivityTypeEnum;
startTime: string;
movingTimeS: number;
calories: number;
activityId: number;
athleteId: number;
name: string;
valuesForSummary: Record<string, string>;
valuesForSummary: Record<string, string> | null;
}