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));
- }
-
-}