Starva Connector

This commit is contained in:
2025-12-24 09:56:57 +01:00
parent b8ef8225e8
commit 69fa9ba5c9
33 changed files with 924 additions and 121 deletions

View File

@@ -13,7 +13,7 @@
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id>
<quarkus.platform.group-id>io.quarkus.platform</quarkus.platform.group-id>
<quarkus.platform.version>3.26.3</quarkus.platform.version>
<quarkus.platform.version>3.30.4</quarkus.platform.version>
<skipITs>true</skipITs>
<surefire-plugin.version>3.5.3</surefire-plugin.version>
</properties>
@@ -39,14 +39,20 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-jackson</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-mongodb-client</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-client</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-client-jackson</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-cache</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-scheduler</artifactId>
@@ -55,9 +61,42 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-arc</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-client-jackson</artifactId>
<artifactId>quarkus-hibernate-orm</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jdbc-postgresql</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-flyway</artifactId>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-database-postgresql</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-jpamodelgen</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.40</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.19.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>1.14.0</version>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
@@ -93,7 +132,17 @@
<artifactId>maven-compiler-plugin</artifactId>
<version>${compiler-plugin.version}</version>
<configuration>
<parameters>true</parameters>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.34</version>
</path>
<path>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-jpamodelgen</artifactId>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>

View File

@@ -1,26 +0,0 @@
package com.peakup;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import java.util.Set;
import java.util.stream.Collectors;
@Path("/hello")
public class ExampleResource {
@Inject
@RestClient
private MyRemoteService myRemoteService;
@GET
@Produces(MediaType.APPLICATION_JSON)
public Hello hello() {
Set<MyRemoteService.Extension> restClientExtensions = myRemoteService.getExtensionsById("io.quarkus:quarkus-hibernate-validator");
return new Hello("Hello from Quarkus REST: " + restClientExtensions.stream().map( MyRemoteService.Extension::getId).collect( Collectors.joining(", ")));
}
}

View File

@@ -1,18 +0,0 @@
package com.peakup;
public class Hello {
private String message;
public Hello(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}

View File

@@ -1,42 +0,0 @@
package com.peakup;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.QueryParam;
import java.util.List;
import java.util.Set;
/**
* To use it via injection.
* <p>
* {@code
*
* @Inject
* @RestClient MyRemoteService myRemoteService;
* <p>
* public void doSomething() {
* Set<MyRemoteService.Extension> restClientExtensions = myRemoteService.getExtensionsById("io.quarkus:quarkus-hibernate-validator");
* }
* }
*/
@RegisterRestClient(baseUri = "https://stage.code.quarkus.io/api")
public interface MyRemoteService {
@GET
@Path("/extensions")
Set<Extension> getExtensionsById(@QueryParam("id") String id);
class Extension {
public String id;
public String name;
public String shortName;
public List<String> keywords;
public String getId() {
return id;
}
}
}

View File

@@ -0,0 +1,15 @@
package com.peakup.athlete.dao;
import com.peakup.athlete.model.Athlete;
import com.peakup.dao.AbstractDAO;
import com.peakup.dao.AbstractTimestampedDAO;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class AthleteDAO extends AbstractTimestampedDAO<Athlete> {
@Override
protected Class<Athlete> getEntityClass() {
return Athlete.class;
}
}

View File

@@ -0,0 +1,22 @@
package com.peakup.athlete.model;
import com.peakup.dao.PersistableEnum;
public enum ActivityType implements PersistableEnum<ActivityType> {
RIDE(1),
RUN(2),
SWIM(3),
OPENWATER(4),
;
private final int key;
ActivityType(int key) {
this.key=key;
}
@Override
public Integer getKey() {
return key;
}
}

View File

@@ -0,0 +1,24 @@
package com.peakup.athlete.model;
import com.peakup.model.AbstractTimestampedEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
@Entity
@Data
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor
@AllArgsConstructor
public class Athlete extends AbstractTimestampedEntity {
@Column
private String firstName;
@Column
private String lastName;
}

View File

@@ -0,0 +1,37 @@
package com.peakup.athlete.model;
import com.peakup.model.AbstractEntity;
import com.peakup.model.AbstractTimestampedEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.OneToOne;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import java.util.List;
@Entity
@Data
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor
@AllArgsConstructor
public class Statistics extends AbstractEntity {
@OneToOne
@JoinColumn(name = "athlete_id")
private Athlete athlete;
@OneToOne
@JoinColumn(name = "total_id")
private StatisticsTotal total;
@OneToOne
@JoinColumn(name = "total_yearly_id")
private StatisticsTotal totalYearly;
@OneToOne
@JoinColumn(name = "total_recent_id")
private StatisticsTotal totalRecent;
}

View File

@@ -0,0 +1,39 @@
package com.peakup.athlete.model;
import com.peakup.dao.AbstractEnumConverter;
import com.peakup.model.AbstractEntity;
import com.peakup.model.AbstractTimestampedEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Convert;
import jakarta.persistence.Entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
@Entity
@Data
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor
@AllArgsConstructor
public class StatisticsTotal extends AbstractTimestampedEntity {
@Convert(converter = AbstractEnumConverter.ActivityTypeConverter.class)
private ActivityType activity;
@Convert(converter = AbstractEnumConverter.StatisticsTypeConverter.class)
private StatisticsType statisticsType;
@Column
private int count;
@Column
private long movingTimeSeconds;
@Column
private long distanceMeter;
@Column
private long elevationMeter;
}

View File

@@ -0,0 +1,31 @@
package com.peakup.athlete.model;
import com.peakup.dao.PersistableEnum;
import org.hibernate.boot.internal.Abstract;
public enum StatisticsType implements PersistableEnum<StatisticsType> {
TOTAL(1),
TOTAL_YEARLY(2),
TOTAL_RECENT(3), // 4 Weeks
TOTAL_DURATION(4)
;
private final int key;
StatisticsType(int key) {
this.key = key;
}
public static StatisticsType fromKey(int key) {
for (StatisticsType e : values()) {
if (e.key == key) return e;
}
throw new IllegalArgumentException("Unknown key: " + key);
}
@Override
public Integer getKey() {
return key;
}
}

View File

@@ -0,0 +1,37 @@
package com.peakup.common;
public class Assert {
private Assert() {}
public static void isTrue(boolean expression, String message) {
if (!expression) {
throw new AssertionError(message);
}
}
public static void isTrue(boolean expression) {
isTrue(expression, null);
}
public static void notNull(Object object, String message) {
if (object == null) {
throw new AssertionError(message);
}
}
public static void notNull(Object object) {
notNull(object, null);
}
public static void isEqual(Object objectA, Object objectB) {
if (objectA == objectB) {
return;
}
if (objectA == null || objectB == null ) {
throw new AssertionError("ObjectA is "+objectA +" and ObjectB is "+objectB);
}
if (!objectA.equals(objectB)) {
throw new AssertionError("ObjectA is "+objectA +" and ObjectB is "+objectB);
}
}
}

View File

@@ -0,0 +1,7 @@
package com.peakup.common;
public class PersistenceException extends RuntimeException {
public PersistenceException(String msg) {
super(msg);
}
}

View File

@@ -0,0 +1,35 @@
package com.peakup.common;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import jakarta.enterprise.context.ApplicationScoped;
import org.apache.commons.lang3.StringUtils;
import org.jspecify.annotations.Nullable;
import java.util.concurrent.TimeUnit;
@ApplicationScoped
public class TokenCache {
private Cache<Long, String> cache = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.SECONDS)
.maximumSize(100)
.<Long, String>build();
public String getBearerToken(Long athleteId) {
String token = cache.getIfPresent(athleteId);
if (StringUtils.isEmpty(token) ) {
}
return token;
}
public void invalidate(Long athleteId) {
cache.invalidate(athleteId);
}
public void invalidateAll() {
cache.invalidateAll();
}
}

View File

@@ -0,0 +1,26 @@
package com.peakup.connector.strava;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import java.util.Map;
@Path("/oauth")
@RegisterRestClient(configKey = "strava-api")
@ApplicationScoped
public interface StravaAuthClient {
@POST
@Path("/token")
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@Produces(MediaType.APPLICATION_JSON)
Map<String, Object> refreshToken(
@FormParam("client_id") String clientId,
@FormParam("client_secret") String clientSecret,
@FormParam("grant_type") String grantType,
@FormParam("refresh_token") String refreshToken
);
}

View File

@@ -0,0 +1,27 @@
package com.peakup.connector.strava;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
@ApplicationScoped
@Path("/api/v3")
@RegisterRestClient(configKey = "strava-api")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public interface StravaClient {
@GET
@Path("/athlete")
Athlete getAthlete(@HeaderParam("Authorization") String bearerToken);
public class Athlete {
public Long id;
public String username;
public String firstname;
public String lastname;
public String city;
public String country;
}
}

View File

@@ -0,0 +1,28 @@
package com.peakup.connector.strava.dao;
import com.peakup.athlete.model.Athlete;
import com.peakup.connector.strava.model.StravaConnector;
import com.peakup.connector.strava.model.StravaConnector_;
import com.peakup.dao.AbstractDAO;
import com.peakup.model.AbstractEntity_;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Root;
@ApplicationScoped
public class StravaConnectorDAO extends AbstractDAO<StravaConnector> {
@Override
protected Class<StravaConnector> getEntityClass() {
return StravaConnector.class;
}
public StravaConnector getByAthleteId(long athleteId) {
final CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<StravaConnector> cq = cb.createQuery(getEntityClass());
Root<StravaConnector> root = cq.from(getEntityClass());
cq.select(root).where(cb.equal(root.get(StravaConnector_.ATHLETE_ID), athleteId));
return em.createQuery(cq).getSingleResult();
}
}

View File

@@ -0,0 +1,39 @@
package com.peakup.connector.strava.model;
import com.peakup.model.AbstractTimestampedEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
@Entity
@Data
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor
@AllArgsConstructor
public class StravaConnector extends AbstractTimestampedEntity {
@Column
private Long athleteId; // loose coupling
@Column
private String stravaAthleteId;
@Column
private String accessToken;
@Column
private String refreshToken;
@Column
private long expiresAt;
@Column
private String clientId;
@Column
private String clientSecret;
}

View File

@@ -0,0 +1,50 @@
package com.peakup.connector.strava.scheduler;
import com.peakup.common.Assert;
import com.peakup.connector.strava.StravaAuthClient;
import com.peakup.connector.strava.dao.StravaConnectorDAO;
import com.peakup.connector.strava.model.StravaConnector;
import io.quarkus.scheduler.Scheduled;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.jboss.logging.Logger;
import java.util.List;
import java.util.Map;
@ApplicationScoped
public class StravaScheduler {
private static final Logger LOG = Logger.getLogger(StravaScheduler.class);
@RestClient
StravaAuthClient stravaAuthClient;
@Inject
StravaConnectorDAO stravaConnectorDAO;
@Scheduled(every = "10m")
@Transactional
void checkRefreshToken() {
long currentTimeInSeconds = System.currentTimeMillis() / 1000;
LOG.info("Current timestamp " +currentTimeInSeconds);
List<StravaConnector> stravaConnectors = stravaConnectorDAO.getAll();
for (StravaConnector stravaConnector : stravaConnectors) {
LOG.info("Strava Connector ID: " + stravaConnector.getAthleteId() + " Token expires at " + stravaConnector.getExpiresAt());
if (currentTimeInSeconds > stravaConnector.getExpiresAt()) {
LOG.info("The token for athlete " +stravaConnector.getAthleteId() + " has been expired for " + (currentTimeInSeconds - stravaConnector.getExpiresAt()) + " seconds." );
Map<String, Object> result = stravaAuthClient.refreshToken(stravaConnector.getClientId(), stravaConnector.getClientSecret(), "refresh_token", stravaConnector.getRefreshToken());
Assert.isEqual(result.get("token_type"), "Bearer");
stravaConnector.setAccessToken(result.get("access_token").toString());
stravaConnector.setRefreshToken(result.get("refresh_token").toString());
stravaConnector.setExpiresAt(currentTimeInSeconds);
stravaConnector.setExpiresAt((Integer)result.get("expires_at"));
stravaConnectorDAO.update(stravaConnector);
}
}
}
}

View File

@@ -0,0 +1,68 @@
package com.peakup.dao;
import com.peakup.common.PersistenceException;
import com.peakup.model.AbstractEntity;
import com.peakup.model.AbstractEntity_;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Root;
import java.util.List;
public abstract class AbstractDAO<T extends AbstractEntity> {
@PersistenceContext
protected EntityManager em;
public T getById(long id) {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<T> cq = cb.createQuery(getEntityClass());
Root<T> root = cq.from(getEntityClass());
cq.select(root).where(cb.equal(root.get(AbstractEntity_.id), id));
return em.createQuery(cq).getSingleResult();
}
public List<T> getAll() {
final CriteriaQuery<T> criteria = getTypedCriteriaQuery();
criteria.select(criteria.from(getEntityClass()));
return em.createQuery(criteria).getResultList();
}
private CriteriaQuery<T> getTypedCriteriaQuery() {
CriteriaBuilder cb = em.getCriteriaBuilder();
final CriteriaQuery<T> criteria = cb.createQuery(getEntityClass());
return criteria;
}
protected abstract Class<T> getEntityClass();
protected EntityManager getEntityManager() {
return em;
}
public void insert(T entity) {
if (entity.getId() != null) {
throw new PersistenceException(
"Entity already has an id, use update() instead: " + entity
);
}
em.persist(entity); // nach persist ist entity attached
}
public void update(T entity) {
if (!em.contains(entity)) {
throw new PersistenceException(
"Entity is not managed (attached). Use merge() or re-fetch before update: " + entity
);
}
// Änderungen werden automatisch von JPA übernommen
}
public void delete(T entity) {
if (!em.contains(entity)) {
entity = em.merge(entity); // sichergehen, dass attached
}
em.remove(entity);
}
}

View File

@@ -0,0 +1,52 @@
package com.peakup.dao;
import com.peakup.athlete.model.ActivityType;
import com.peakup.athlete.model.StatisticsType;
import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter;
public abstract class AbstractEnumConverter<V extends PersistableEnum> implements AttributeConverter<V, Integer> {
protected AbstractEnumConverter() {
super();
}
@Override
public Integer convertToDatabaseColumn(V attribute) {
return (attribute != null) ? attribute.getKey() : null;
}
@Override
public V convertToEntityAttribute(Integer dbData) {
if (dbData == null) return null;
return getEnumByKey(dbData);
}
public abstract V getEnumByKey(Integer key);
@Converter(autoApply = true)
public static class StatisticsTypeConverter extends AbstractEnumConverter<StatisticsType> {
public StatisticsTypeConverter() {
super();
}
@Override
public StatisticsType getEnumByKey(Integer key) {
return PersistableEnum.getEnumByKey(key, StatisticsType.values());
}
}
@Converter(autoApply = true)
public static class ActivityTypeConverter extends AbstractEnumConverter<ActivityType> {
public ActivityTypeConverter() {
super();
}
@Override
public ActivityType getEnumByKey(Integer key) {
return PersistableEnum.getEnumByKey(key, ActivityType.values());
}
}
}

View File

@@ -0,0 +1,20 @@
package com.peakup.dao;
import com.peakup.common.PersistenceException;
import com.peakup.model.AbstractEntity;
import com.peakup.model.AbstractEntity_;
import com.peakup.model.AbstractTimestampedEntity;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Root;
import java.time.LocalDateTime;
import java.util.List;
public abstract class AbstractTimestampedDAO<T extends AbstractTimestampedEntity> extends AbstractDAO<T> {
}

View File

@@ -0,0 +1,12 @@
package com.peakup.dao;
public interface PersistableEnum<V extends PersistableEnum > {
Integer getKey();
static <T extends PersistableEnum> T getEnumByKey(Integer key, T[] values) {
for (T v : values) {
if (v.getKey().equals( key)) return v;
}
throw new IllegalArgumentException("Unknown key: " + key);
}
}

View File

@@ -0,0 +1,20 @@
package com.peakup.model;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@MappedSuperclass
@Data
@NoArgsConstructor
@AllArgsConstructor
public abstract class AbstractEntity {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "global_seq")
@SequenceGenerator(name = "global_seq", sequenceName = "global_id_seq", allocationSize = 1)
@Column(name = "ID")
private Long id;
}

View File

@@ -0,0 +1,25 @@
package com.peakup.model;
import jakarta.persistence.Column;
import jakarta.persistence.MappedSuperclass;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@MappedSuperclass
@Data
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor
@AllArgsConstructor
public abstract class AbstractTimestampedEntity extends AbstractEntity {
@Column
private LocalDateTime modifiedAt;
@Column
private LocalDateTime createdAt;
}

View File

@@ -0,0 +1,16 @@
quarkus.http.port=8111
quarkus.datasource.db-kind=postgresql
quarkus.datasource.username=peakup
quarkus.datasource.password=peakup
quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/peakup
# Flyway aktivieren
quarkus.hibernate-orm.sql-load-script=no-file
quarkus.hibernate-orm.physical-naming-strategy=org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy
quarkus.flyway.migrate-at-start=true
quarkus.flyway.clean-at-start=false
# strava
strava-api/mp-rest/url=https://www.strava.com

View File

@@ -0,0 +1,23 @@
-- Tabelle für Athlete
CREATE TABLE athlete (
id BIGSERIAL PRIMARY KEY, -- Auto-Inkrement
first_name VARCHAR(255),
last_name VARCHAR(255),
created_at TIMESTAMP DEFAULT NOW(),
modified_at TIMESTAMP DEFAULT NOW()
);
-- Tabelle für StravaConnector
CREATE TABLE strava_connector (
id BIGSERIAL PRIMARY KEY, -- Auto-Inkrement
athlete_id BIGINT UNIQUE, -- Unique Constraint
strava_athlete_id VARCHAR(255),
access_token VARCHAR(512),
refresh_token VARCHAR(512),
expires_at BIGINT,
client_id VARCHAR(255),
client_secret VARCHAR(255),
created_at TIMESTAMP DEFAULT NOW(),
modified_at TIMESTAMP DEFAULT NOW(),
CONSTRAINT fk_athlete FOREIGN KEY (athlete_id) REFERENCES athlete(id)
);

View File

@@ -0,0 +1,4 @@
-- ============================================
-- GLOBAL SEQUENCE für alle Entities
-- ============================================
CREATE SEQUENCE global_id_seq START WITH 100 INCREMENT BY 1;

View File

@@ -0,0 +1,75 @@
-- ============================================
-- Flyway Migration: Create Statistics and StatisticsTotal Tables
-- ============================================
-- Optional: falls du Sequenzen zur ID-Generierung nutzt:
-- CREATE SEQUENCE statistics_total_id_seq START WITH 1 INCREMENT BY 1;
-- CREATE SEQUENCE statistics_id_seq START WITH 1 INCREMENT BY 1;
-- ==================================================
-- TABLE: STATISTICS_TOTAL
-- ==================================================
CREATE TABLE statistics_total (
id BIGINT PRIMARY KEY,
activity INTEGER, -- Enum: ActivityType
statistics_type INTEGER, -- Enum: StatisticsType
count INT NOT NULL,
moving_time_seconds BIGINT NOT NULL,
distance_meter BIGINT NOT NULL,
elevation_meter BIGINT NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),
modified_at TIMESTAMP DEFAULT NOW()
);
-- Optional: falls du Trigger für automatische updated_at-Pflege willst:
-- CREATE OR REPLACE FUNCTION update_updated_at_column()
-- RETURNS TRIGGER AS $$
-- BEGIN
-- NEW.updated_at = NOW();
-- RETURN NEW;
-- END;
-- $$ language 'plpgsql';
--
-- CREATE TRIGGER update_statistics_total_updated_at
-- BEFORE UPDATE ON statistics_total
-- FOR EACH ROW
-- EXECUTE PROCEDURE update_updated_at_column();
-- ==================================================
-- TABLE: STATISTICS
-- ==================================================
CREATE TABLE statistics (
id BIGINT PRIMARY KEY,
athlete_id BIGINT, -- Verweis auf Athlete
total_id BIGINT,
total_yearly_id BIGINT,
total_recent_id BIGINT,
CONSTRAINT fk_statistics_athlete
FOREIGN KEY (athlete_id)
REFERENCES athlete (id)
ON DELETE SET NULL,
CONSTRAINT fk_statistics_total
FOREIGN KEY (total_id)
REFERENCES statistics_total (id)
ON DELETE SET NULL,
CONSTRAINT fk_statistics_total_yearly
FOREIGN KEY (total_yearly_id)
REFERENCES statistics_total (id)
ON DELETE SET NULL,
CONSTRAINT fk_statistics_total_recent
FOREIGN KEY (total_recent_id)
REFERENCES statistics_total (id)
ON DELETE SET NULL
);
-- ==================================================
-- INDEXES
-- ==================================================
CREATE INDEX idx_statistics_athlete_id ON statistics (athlete_id);
CREATE INDEX idx_statistics_total_id ON statistics (total_id);
CREATE INDEX idx_statistics_total_yearly_id ON statistics (total_yearly_id);
CREATE INDEX idx_statistics_total_recent_id ON statistics (total_recent_id);

View File

@@ -1,8 +0,0 @@
package com.peakup;
import io.quarkus.test.junit.QuarkusIntegrationTest;
@QuarkusIntegrationTest
class ExampleResourceIT extends ExampleResourceTest {
// Execute the same tests but in packaged mode.
}

View File

@@ -1,20 +0,0 @@
package com.peakup;
import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.is;
@QuarkusTest
class ExampleResourceTest {
@Test
void testHelloEndpoint() {
given()
.when().get("/hello")
.then()
.statusCode(200)
.body(is("Hello from Quarkus REST"));
}
}

View File

@@ -0,0 +1,36 @@
package com.peakup.dao;
import com.peakup.athlete.dao.AthleteDAO;
import com.peakup.athlete.model.Athlete;
import com.peakup.connector.strava.dao.StravaConnectorDAO;
import com.peakup.connector.strava.model.StravaConnector;
import io.quarkus.test.junit.QuarkusTest;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
@QuarkusTest
class DAOTest {
@Inject
AthleteDAO athleteDAO;
@Inject
StravaConnectorDAO stravaConnectorDAO;
@Test
@Transactional
void testGetAthleteById() {
Athlete found = athleteDAO.getById(1);
assertNotNull(found);
}
@Test
@Transactional
void testGetStravaConnctorByAthleteId() {
StravaConnector found = stravaConnectorDAO.getByAthleteId(1);
assertNotNull(found);
}
}

View File

100
resources/strava-api.json Normal file
View File

@@ -0,0 +1,100 @@
{
"_type": "export",
"__export_format": 4,
"__export_date": "2025-09-19T12:00:00.000Z",
"__export_source": "insomnia.desktop.app:v2025.1.0",
"resources": [
{
"_id": "wrk_strava",
"_type": "workspace",
"name": "Strava API",
"scope": "collection"
},
{
"_id": "env_strava",
"_type": "environment",
"parentId": "wrk_strava",
"name": "Base Environment",
"data": {
"client_id": "YOUR_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET",
"redirect_uri": "http://localhost/exchange_token",
"access_token": "",
"refresh_token": "",
"athlete_id": ""
}
},
{
"_id": "req_auth_token",
"_type": "request",
"parentId": "wrk_strava",
"name": "Get Access Token",
"method": "POST",
"url": "https://www.strava.com/oauth/token",
"body": {
"mimeType": "application/x-www-form-urlencoded",
"params": [
{ "name": "client_id", "value": "{{ client_id }}" },
{ "name": "client_secret", "value": "{{ client_secret }}" },
{ "name": "code", "value": "AUTHORIZATION_CODE" },
{ "name": "grant_type", "value": "authorization_code" }
]
}
},
{
"_id": "req_refresh_token",
"_type": "request",
"parentId": "wrk_strava",
"name": "Refresh Access Token",
"method": "POST",
"url": "https://www.strava.com/oauth/token",
"body": {
"mimeType": "application/x-www-form-urlencoded",
"params": [
{ "name": "client_id", "value": "{{ client_id }}" },
{ "name": "client_secret", "value": "{{ client_secret }}" },
{ "name": "grant_type", "value": "refresh_token" },
{ "name": "refresh_token", "value": "{{ refresh_token }}" }
]
}
},
{
"_id": "req_athlete",
"_type": "request",
"parentId": "wrk_strava",
"name": "Get Logged-In Athlete",
"method": "GET",
"url": "https://www.strava.com/api/v3/athlete",
"headers": [
{ "name": "Authorization", "value": "Bearer {{ access_token }}" }
]
},
{
"_id": "req_activities",
"_type": "request",
"parentId": "wrk_strava",
"name": "Get Athlete Activities",
"method": "GET",
"url": "https://www.strava.com/api/v3/athlete/activities",
"headers": [
{ "name": "Authorization", "value": "Bearer {{ access_token }}" }
],
"parameters": [
{ "name": "per_page", "value": "10" },
{ "name": "page", "value": "1" }
]
},
{
"_id": "req_activity",
"_type": "request",
"parentId": "wrk_strava",
"name": "Get Activity by ID",
"method": "GET",
"url": "https://www.strava.com/api/v3/activities/{{ activity_id }}",
"headers": [
{ "name": "Authorization", "value": "Bearer {{ access_token }}" }
]
}
]
}