diff --git a/src/main/java/com/brennaswitzer/cookbook/domain/Timer.java b/src/main/java/com/brennaswitzer/cookbook/domain/Timer.java deleted file mode 100644 index 58600f61..00000000 --- a/src/main/java/com/brennaswitzer/cookbook/domain/Timer.java +++ /dev/null @@ -1,153 +0,0 @@ -package com.brennaswitzer.cookbook.domain; - -import jakarta.persistence.Column; -import jakarta.persistence.Embedded; -import jakarta.persistence.Entity; -import jakarta.validation.constraints.NotNull; -import lombok.Getter; -import lombok.Setter; -import lombok.val; - -import java.time.Duration; -import java.time.Instant; - -/** - * I represent a user-controlled timer with second granularity. A timer can be - * in three states: running, paused, and complete. It starts out running, and - * may be paused and then resumed any number of times. Extra time may be added - * until complete as well. Once its duration plus extra time has elapsed, it - * becomes complete and may no longer be paused or have time added. - */ -@Entity -public class Timer extends BaseEntity implements AccessControlled { - - @Embedded - @NotNull - @Getter - @Setter - private Acl acl = new Acl(); - - @Column(updatable = false) - @Getter - private int duration; - - @Getter - private int extraTime; - - @Getter - @Setter - private Instant pausedAt; - - public void setDuration(int duration) { - if (duration <= 0) { - throw new IllegalArgumentException("Timer durations must be positive, but " + duration + " isn't."); - } - if (this.duration > 0) { - throw new UnsupportedOperationException("Timer durations cannot be changed; do you want to add extra time?"); - } - this.duration = duration; - } - - public void addExtraTime(int extraTime) { - addExtraTime(extraTime, Instant.now()); - } - - public void addExtraTime(int extraTime, Instant asOf) { - if (isComplete(asOf)) { - throw new IllegalStateException("Extra time cannot be added after a timer is complete"); - } - this.extraTime += extraTime; - } - - public boolean isPaused() { - return isPaused(Instant.now()); - } - - public boolean isPaused(Instant asOf) { - return pausedAt != null && pausedAt.isBefore(asOf); - } - - public boolean isComplete() { - return isComplete(Instant.now()); - } - - public boolean isComplete(Instant asOf) { - return getRemaining(asOf) <= 0; - } - - public boolean isRunning() { - return isRunning(Instant.now()); - } - - public boolean isRunning(Instant asOf) { - return !isPaused() && getRemaining(asOf) > 0; - } - - /** - * I return the amount of time remaining on the timer, which may be negative - * if it has already completed. - */ - public int getRemaining() { - return getRemaining(Instant.now()); - } - - public int getRemaining(Instant asOf) { - var end = getEndAt(asOf); - return between(asOf, end); - } - - private int between(Instant start, Instant end) { - val dur = Duration.between(start, end); - val sec = (int) dur.getSeconds(); - return dur.getNano() < 500_000_000 ? sec : (sec + 1); - } - - /** - * I return when the timer will end, or has ended if it has already - * completed. If the timer is currently paused, return the end time as - * if it were immediately resumed. - */ - public Instant getEndAt() { - return getEndAt(Instant.now()); - } - - public Instant getEndAt(Instant asOf) { - var base = getCreatedAt() - .plusSeconds(duration) - .plusSeconds(extraTime); -// base = base.minusNanos(base.getNano()); - if (isPaused(asOf)) - base = base.plusSeconds(between(pausedAt, asOf)); - return base; - } - - public void pause() { - pause(Instant.now()); - } - - public void pause(Instant asOf) { - if (pausedAt != null) { - throw new IllegalStateException("Timer is already paused"); - } - if (isComplete(asOf)) { - throw new IllegalStateException("Timer is already complete"); - } - pausedAt = asOf; - } - - public void resume() { - resume(Instant.now()); - } - - public void resume(Instant asOf) { - if (pausedAt == null) { - throw new IllegalStateException("Timer is not paused"); - } - if (asOf.isBefore(pausedAt)) { - throw new IllegalArgumentException("Resume at " + asOf + " makes no sense; not paused until " + pausedAt); - } - extraTime += between(pausedAt, asOf); - pausedAt = null; - } - -} diff --git a/src/main/java/com/brennaswitzer/cookbook/graphql/Mutation.java b/src/main/java/com/brennaswitzer/cookbook/graphql/Mutation.java index ed532213..d9a336ce 100644 --- a/src/main/java/com/brennaswitzer/cookbook/graphql/Mutation.java +++ b/src/main/java/com/brennaswitzer/cookbook/graphql/Mutation.java @@ -13,9 +13,6 @@ public class Mutation implements GraphQLMutationResolver { @Autowired public LibraryMutation library; - @Autowired - public TimerMutation timer; - @Autowired public FavoriteMutation favorite; diff --git a/src/main/java/com/brennaswitzer/cookbook/graphql/Query.java b/src/main/java/com/brennaswitzer/cookbook/graphql/Query.java index a7d6eaf4..2b0ac0d7 100644 --- a/src/main/java/com/brennaswitzer/cookbook/graphql/Query.java +++ b/src/main/java/com/brennaswitzer/cookbook/graphql/Query.java @@ -23,9 +23,6 @@ public class Query implements GraphQLQueryResolver { @Autowired private UserPrincipalAccess userPrincipalAccess; - @Autowired - public TimerQuery timer; - @Autowired public FavoriteQuery favorite; diff --git a/src/main/java/com/brennaswitzer/cookbook/graphql/TimerMutation.java b/src/main/java/com/brennaswitzer/cookbook/graphql/TimerMutation.java deleted file mode 100644 index 22ae5431..00000000 --- a/src/main/java/com/brennaswitzer/cookbook/graphql/TimerMutation.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.brennaswitzer.cookbook.graphql; - -import com.brennaswitzer.cookbook.domain.Timer; -import com.brennaswitzer.cookbook.graphql.model.Deletion; -import com.brennaswitzer.cookbook.services.timers.UpdateTimers; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; - -@SuppressWarnings("unused") -@Component -public class TimerMutation { - - @Autowired - private UpdateTimers update; - - public Timer create(int duration) { - return update.createTimer(duration); - } - - public Timer pause(Long id) { - return update.pauseTimer(id); - } - - public Timer resume(Long id) { - return update.resumeTimer(id); - } - - public Timer addTime(Long id, int duration) { - return update.addTime(id, duration); - } - - public Deletion delete(Long id) { - return new Deletion(update.deleteTimer(id).getId(), - "Unnamed Timer"); - } - -} diff --git a/src/main/java/com/brennaswitzer/cookbook/graphql/TimerQuery.java b/src/main/java/com/brennaswitzer/cookbook/graphql/TimerQuery.java deleted file mode 100644 index abb89d15..00000000 --- a/src/main/java/com/brennaswitzer/cookbook/graphql/TimerQuery.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.brennaswitzer.cookbook.graphql; - -import com.brennaswitzer.cookbook.domain.Timer; -import com.brennaswitzer.cookbook.services.timers.FetchTimers; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; - -@SuppressWarnings("unused") -@Component -public class TimerQuery { - - @Autowired - private FetchTimers fetch; - - public Iterable all() { - return fetch.getTimersForUser(); - } - - public Timer byId(Long id) { - return fetch.getTimerById(id); - } - -} diff --git a/src/main/java/com/brennaswitzer/cookbook/graphql/resolvers/TimerResolver.java b/src/main/java/com/brennaswitzer/cookbook/graphql/resolvers/TimerResolver.java deleted file mode 100644 index 22af6da6..00000000 --- a/src/main/java/com/brennaswitzer/cookbook/graphql/resolvers/TimerResolver.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.brennaswitzer.cookbook.graphql.resolvers; - -import com.brennaswitzer.cookbook.domain.Timer; -import graphql.kickstart.tools.GraphQLResolver; -import org.springframework.stereotype.Component; - -import java.time.Instant; -import java.time.OffsetDateTime; -import java.time.ZoneOffset; -import java.util.List; - -/** - * Timers are projected with initialDuration/duration, while the business layer - * considers duration/extraTime. - */ -@SuppressWarnings("unused") // component-scanned for graphql-java -@Component -public class TimerResolver implements GraphQLResolver { - - public List grants(Timer timer) { - return AclHelpers.getGrants(timer); - } - - public int initialDuration(Timer timer) { - return timer.getDuration(); - } - - public int duration(Timer timer) { - return timer.getDuration() + timer.getExtraTime(); - } - - /** - * GraphQL Java speaks {@link OffsetDateTime}, not {@link Instant}, so - * convert. - */ - public OffsetDateTime endAt(Timer timer) { - if (timer.isPaused()) return null; - return timer.getEndAt() - .atOffset(ZoneOffset.UTC); - } - -} diff --git a/src/main/java/com/brennaswitzer/cookbook/repositories/TimerRepository.java b/src/main/java/com/brennaswitzer/cookbook/repositories/TimerRepository.java deleted file mode 100644 index d8e2c3b1..00000000 --- a/src/main/java/com/brennaswitzer/cookbook/repositories/TimerRepository.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.brennaswitzer.cookbook.repositories; - -import com.brennaswitzer.cookbook.domain.Timer; -import com.brennaswitzer.cookbook.domain.User; - -public interface TimerRepository extends BaseEntityRepository { - - Iterable findByAclOwnerOrderByCreatedAt(User owner); - -} diff --git a/src/main/java/com/brennaswitzer/cookbook/services/timers/FetchTimers.java b/src/main/java/com/brennaswitzer/cookbook/services/timers/FetchTimers.java deleted file mode 100644 index 37173cc9..00000000 --- a/src/main/java/com/brennaswitzer/cookbook/services/timers/FetchTimers.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.brennaswitzer.cookbook.services.timers; - -import com.brennaswitzer.cookbook.domain.AccessLevel; -import com.brennaswitzer.cookbook.domain.Timer; -import com.brennaswitzer.cookbook.repositories.TimerRepository; -import com.brennaswitzer.cookbook.util.UserPrincipalAccess; -import lombok.val; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -@Service -@Transactional -public class FetchTimers { - - @Autowired - private TimerRepository repo; - - @Autowired - private UserPrincipalAccess principalAccess; - - public Iterable getTimersForUser() { - // todo: use the ACL, not just ownership - return repo.findByAclOwnerOrderByCreatedAt(principalAccess.getUser()); - } - - public Timer getTimerById(Long id) { - val timer = repo.getReferenceById(id); - timer.ensurePermitted(principalAccess.getUser(), AccessLevel.VIEW); - return timer; - } - -} diff --git a/src/main/java/com/brennaswitzer/cookbook/services/timers/UpdateTimers.java b/src/main/java/com/brennaswitzer/cookbook/services/timers/UpdateTimers.java deleted file mode 100644 index ebfe085a..00000000 --- a/src/main/java/com/brennaswitzer/cookbook/services/timers/UpdateTimers.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.brennaswitzer.cookbook.services.timers; - -import com.brennaswitzer.cookbook.domain.AccessLevel; -import com.brennaswitzer.cookbook.domain.Timer; -import com.brennaswitzer.cookbook.repositories.TimerRepository; -import com.brennaswitzer.cookbook.util.UserPrincipalAccess; -import lombok.val; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -@Service -@Transactional -public class UpdateTimers { - - @Autowired - private TimerRepository repo; - - @Autowired - private UserPrincipalAccess principalAccess; - - public Timer createTimer(int duration) { - val t = new Timer(); - t.setOwner(principalAccess.getUser()); - t.setDuration(duration); - return repo.save(t); - } - - public Timer pauseTimer(Long id) { - val t = repo.getReferenceById(id); - t.ensurePermitted(principalAccess.getUser(), AccessLevel.CHANGE); - t.pause(); - return t; - } - - public Timer resumeTimer(Long id) { - val t = repo.getReferenceById(id); - t.ensurePermitted(principalAccess.getUser(), AccessLevel.CHANGE); - t.resume(); - return t; - } - - public Timer addTime(Long id, int duration) { - val t = repo.getReferenceById(id); - t.ensurePermitted(principalAccess.getUser(), AccessLevel.CHANGE); - t.addExtraTime(duration); - return t; - } - - public Timer deleteTimer(Long id) { - val t = repo.getReferenceById(id); - t.ensurePermitted(principalAccess.getUser(), AccessLevel.CHANGE); - repo.delete(t); - return t; - } - -} diff --git a/src/main/resources/db/changelog/gobrennas-2024.sql b/src/main/resources/db/changelog/gobrennas-2024.sql index 79463051..2798b2fd 100644 --- a/src/main/resources/db/changelog/gobrennas-2024.sql +++ b/src/main/resources/db/changelog/gobrennas-2024.sql @@ -422,3 +422,7 @@ where r.id = h.recipe_id; alter table planned_recipe_history alter owner_id set not null, alter done_at set not null; + +--changeset barneyb:remove-timers +drop table timer_grants; +drop table timer; diff --git a/src/main/resources/graphqls/timers.graphqls b/src/main/resources/graphqls/timers.graphqls deleted file mode 100644 index f8546781..00000000 --- a/src/main/resources/graphqls/timers.graphqls +++ /dev/null @@ -1,61 +0,0 @@ -extend type Query { - timer: TimerQuery -} - -extend type Mutation { - timer: TimerMutation -} - -type TimerQuery { - all: [Timer!]! - byId(id: ID!): Timer! -} - -type TimerMutation { - """Create a new timer with the specified duration and start it. - """ - create(duration: PositiveInt!): Timer! - """Pause the specified running timer. - """ - pause(id: ID!): Timer! - """Resume the specified paused timer. - """ - resume(id: ID!): Timer! - """Add the specified duration to the specified timer, which may not be - complete, but may be paused. - """ - addTime(id: ID!, duration: PositiveInt!): Timer! - """Ensure the specified timer has been deleted, regardless of its status or - existence, returning whether any action was taken. - """ - delete(id: ID!): Deletion! -} - -"""Represents a pause-able timer of user-specified length. -""" -type Timer implements Node & Owned & AccessControlled { - id: ID! - owner: User! - grants: [AccessControlEntry!]! - """Number of seconds the timer was originally created for. - """ - initialDuration: PositiveInt! - """Number of seconds the timer is currently set for. - """ - duration: PositiveInt! - """When the timer reached or will reach completion; null if paused. - """ - endAt: DateTime, - """Number of seconds remaining; negative if already complete. - """ - remaining: Int! - """Whether the timer is running. - """ - running: Boolean! - """Whether the timer is paused. - """ - paused: Boolean! - """Whether the timer is complete. - """ - complete: Boolean! -} diff --git a/src/test/java/com/brennaswitzer/cookbook/domain/TimerTest.java b/src/test/java/com/brennaswitzer/cookbook/domain/TimerTest.java deleted file mode 100644 index 913a6596..00000000 --- a/src/test/java/com/brennaswitzer/cookbook/domain/TimerTest.java +++ /dev/null @@ -1,211 +0,0 @@ -package com.brennaswitzer.cookbook.domain; - -import lombok.val; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - -import java.time.Duration; -import java.time.Instant; -import java.time.temporal.ChronoUnit; - -import static org.junit.jupiter.api.Assertions.*; - -class TimerTest { - - // Sat Dec 31 2022 12:00:00 PST - private static final Instant NOW = Instant.ofEpochSecond(1672516800); - - private Instant in(int seconds) { - return NOW.plus(seconds, ChronoUnit.SECONDS); - } - - private Timer setFor(int seconds) { - return setFor(seconds, NOW); - } - - private Timer setFor(int seconds, Instant at) { - val t = new Timer(); - t.setCreatedAt(at); - t.setDuration(seconds); - return t; - } - - /* - * This may be flaky, hence being @Disabled. There's a reason for all those - * helper methods that accept an Instant, instead of relying on the clock - * of the computer to tell time. - */ - @Test - @Disabled - void doesItSmoke() throws InterruptedException { - val t = new Timer(); - t.setCreatedAt(Instant.now()); - t.setDuration(1); - assertEquals(1, t.getRemaining()); - Thread.sleep(200); - assertTrue(t.isRunning()); - assertFalse(t.isPaused()); - assertFalse(t.isComplete()); - assertEquals(1, t.getRemaining()); - t.pause(); - assertFalse(t.isRunning()); - assertTrue(t.isPaused()); - assertFalse(t.isComplete()); - assertEquals(1, t.getRemaining()); - Thread.sleep(800); - assertFalse(t.isRunning()); - assertTrue(t.isPaused()); - assertFalse(t.isComplete()); - assertEquals(1, t.getRemaining()); - t.resume(); - assertTrue(t.isRunning()); - assertFalse(t.isPaused()); - assertFalse(t.isComplete()); - assertEquals(1, t.getRemaining()); - Thread.sleep(1100); - assertFalse(t.isRunning()); - assertFalse(t.isPaused()); - assertTrue(t.isComplete()); - assertEquals(0, t.getRemaining()); - } - - @Test - void getEndAt() { - val t = setFor(15); - assertEquals(in(15), t.getEndAt()); - t.addExtraTime(5, in(10)); - assertEquals(in(20), t.getEndAt()); - t.pause(in(15)); - // paused w/ five seconds left, so always five seconds from now - var now = in(123456); - assertEquals(5, Duration.between(now, t.getEndAt(now)).getSeconds()); - t.resume(in(18)); - assertEquals(in(23), t.getEndAt()); - } - - @Test - void getRemaining_neverPaused() { - val t = setFor(15); - assertEquals(7, t.getRemaining(in(8))); - assertEquals(5, t.getRemaining(in(10))); - assertEquals(0, t.getRemaining(in(15))); - assertEquals(-2, t.getRemaining(in(17))); - } - - @Test - void getRemaining_currentlyPaused() { - val t = setFor(15); - t.pause(in(5)); - assertEquals(10, t.getRemaining(in(5))); - assertEquals(10, t.getRemaining(in(6))); - assertEquals(10, t.getRemaining(in(99999))); - } - - @Test - void getRemaining_previouslyAndCurrentlyPaused() { - val t = setFor(15); - t.addExtraTime(6, in(6)); - t.setPausedAt(in(10)); - assertEquals(11, t.getRemaining(in(10))); - assertEquals(11, t.getRemaining(in(50))); - t.resume(in(30)); - assertEquals(11, t.getRemaining(in(30))); - assertEquals(6, t.getRemaining(in(35))); - assertEquals(-9, t.getRemaining(in(50))); - } - - @Test - void isCompleteOrRunning() { - val t = setFor(10); - assertFalse(t.isComplete(in(5))); - assertTrue(t.isRunning(in(5))); - assertTrue(t.isComplete(in(10))); - assertFalse(t.isRunning(in(10))); - assertTrue(t.isComplete(in(15))); - assertFalse(t.isRunning(in(15))); - } - - @Test - void isPausedOrRunning() { - val t = setFor(10); - assertFalse(t.isPaused()); - assertTrue(t.isRunning(in(2))); - t.pause(in(5)); - assertTrue(t.isPaused()); - assertFalse(t.isRunning(in(6))); - t.resume(in(7)); - assertFalse(t.isPaused()); - assertTrue(t.isRunning(in(9))); - assertTrue(t.isRunning(in(11))); - // complete at 12 - assertFalse(t.isPaused()); - assertFalse(t.isRunning(in(13))); - } - - @Test - void extraTime() { - val t = setFor(10); - assertEquals(10, t.getRemaining(NOW)); - assertEquals(0, t.getRemaining(in(10))); - assertTrue(t.isComplete(in(12))); - - t.addExtraTime(5, in(5)); - - assertEquals(15, t.getRemaining(NOW)); - assertEquals(5, t.getRemaining(in(10))); - assertFalse(t.isComplete(in(12))); - assertEquals(-1, t.getRemaining(in(16))); - } - - @Test - void cantPauseCompleted() { - assertThrows(IllegalStateException.class, () -> - setFor(10).pause(in(15))); - } - - @Test - void cantPausePaused() { - val t = setFor(10); - t.pause(in(5)); - assertThrows(IllegalStateException.class, () -> - t.pause(in(7))); - } - - @Test - void cantResumeRunning() { - val t = setFor(10); - assertThrows(IllegalStateException.class, () -> - t.resume(in(1))); - t.pause(in(3)); - t.resume(in(5)); - assertThrows(IllegalStateException.class, () -> - t.resume(in(7))); - } - - @Test - void cantResumeBeforePause() { - val t = setFor(10); - t.pause(in(3)); - assertThrows(IllegalArgumentException.class, () -> - t.resume(in(1))); - } - - @Test - void cantSetNegativeDuration() { - assertThrows(IllegalArgumentException.class, () -> - setFor(-1)); - } - - @Test - void cantChangeDuration() { - assertThrows(UnsupportedOperationException.class, () -> - setFor(10).setDuration(15)); - } - - @Test - void cantAddTimeToCompleted() { - assertThrows(IllegalStateException.class, () -> - setFor(1, in(-10)).addExtraTime(1)); - } - -}