Break out api module for timeline.
[?]
Aug 22, 2020, 4:31 AM
RSF6UAJKG7CEKILSVXI6C4YZXY7PIYZM2EMA2IXKQ7SADKNVSH7QCDependencies
- [2]
QMEYU4MWAdd display for prior intervals. - [3]
HO2PFRABClient login now handles response correctly. - [4]
B6HWAPDPModularize & update to recent haskoin. - [5]
73NDXDEZBegin implementation of billing event persistence. - [6]
RPAJLHMTChange to use UUIDs instead of ints for primary keys. - [7]
4U7F3CPITHE GREAT RENAMING OF THINGS! - [8]
2XQD6KKKAdd invitation logic and clean up DBProg error handling. - [9]
ZIG57EE6Fix project selection, end log end on project switch. - [10]
EFSXYZPOAutoformat everything with brittany. - [11]
NEDDHXUKReformat via stylish-haskell - [12]
NJNMO72SAdd zcash.com submodule and update client to modern halogen. - [13]
AL37SVTCImplement payments service endpoints. - [14]
QU5FW67RAdd project selection to time tracker. - [15]
O722AOKEAdd route to allow crediting of events to users/projects. - [16]
GMYPBCWEMake docker-compose work. - [17]
JXG3FCXYUpgrade ps + halogen versions. - [18]
AWWC6P5ZAdd migration to include payment network with addresses. - [19]
J6S23MDGUse server timestamps for interval start and end. - [20]
HALRDT2FAdded initial auction create route. - [21]
GCVQD44VCreate amends endpoint, switch to UUID primary keys - [22]
BFZN4SUAMake timeline component work. - [23]
O5FVTOM6Undo JSON silliness, enable a couple more routes. - [24]
IPG33FAWAdd billing daemon - [25]
4R7XIYK3Switch from ClassyPrelude to Relude - [26]
MGOF7IUFUpdate TASKS list to reflect completed projects. - [*]
EA5BFM5GSplit Login component into its own module. - [*]
IZEVQF62Work in progress replacing sqlite with postgres. - [*]
A6HKMINBAttempting to improve JSON handling. - [*]
64C6AWH6Rename Ananke -> Quixotic, project reboot. - [*]
HMDM3B55Implement core of payments/billing infrastructure. - [*]
BROSTG5KBeginning of modularization of server. - [*]
ADMKQQGCInitial empty Snap project.
Change contents
- file addition: Api[28.1]
- file addition: Timeline.purs[0.1]
module Aftok.Api.Timeline whereimport Preludeimport Control.Alt ((<|>))import Control.Monad.Error.Class (throwError)import Control.Monad.Except.Trans (withExceptT, runExceptT)import Data.Argonaut.Decode (class DecodeJson, decodeJson, (.:), (.:?))import Data.DateTime (DateTime)import Data.DateTime.Instant (Instant)import Data.Either (Either, note)import Data.Foldable (class Foldable, foldMapDefaultR, intercalate, foldr, foldl)import Data.JSDate as JDimport Data.Maybe (Maybe(..))import Data.Newtype (class Newtype)import Data.Traversable (class Traversable, traverse)import Data.UUID as UUIDimport Type.Proxy (Proxy(..))-- import Text.Format as F -- (format, zeroFill, width)import Effect.Aff (Aff)import Effect.Class (liftEffect)import Affjax (get, post)import Affjax.RequestBody as RBimport Affjax.ResponseFormat as RFimport Data.Argonaut.Encode (encodeJson)import Aftok.Project (ProjectId(..), pidStr)import Aftok.Types (APIError, JsonCompose, decompose, parseDatedResponse)data TimelineError= LogFailure (APIError)| Unexpected Stringinstance showTimelineError :: Show TimelineError whereshow = case _ ofLogFailure e -> show eUnexpected t -> tdata Event' i= StartEvent i| StopEvent itype Event = Event' Instantderive instance eventFunctor :: Functor Event'instance eventFoldable :: Foldable Event' wherefoldr f b = case _ ofStartEvent a -> f a bStopEvent a -> f a bfoldl f b = case _ ofStartEvent a -> f b aStopEvent a -> f b afoldMap = foldMapDefaultRinstance eventTraversable :: Traversable Event' wheretraverse f = case _ ofStartEvent a -> StartEvent <$> f aStopEvent a -> StopEvent <$> f asequence = traverse identityinstance decodeJsonEvent :: DecodeJson (Event' String) wheredecodeJson json = doobj <- decodeJson jsonevent <- obj .: "event"start' <- traverse (_ .: "eventTime") =<< event .:? "start"stop' <- traverse (_ .: "eventTime") =<< event .:? "stop"note "Only 'stop' and 'start' events are supported." $ (StartEvent <$> start') <|> (StopEvent <$> stop')newtype Interval' i = Interval{ start :: i, end :: i}derive instance intervalEq :: (Eq i) => Eq (Interval' i)derive instance intervalNewtype :: Newtype (Interval' i) _type Interval = Interval' Instantderive instance intervalFunctor :: Functor Interval'instance intervalFoldable :: Foldable Interval' wherefoldr f b (Interval i) = f i.start (f i.end b)foldl f b (Interval i) = f (f b i.start) i.endfoldMap = foldMapDefaultRinstance intervalTraversable :: Traversable Interval' wheretraverse f (Interval i) = interval <$> f i.start <*> f i.endsequence = traverse identityinstance decodeJsonInterval :: DecodeJson (Interval' String) wheredecodeJson json = doobj <- decodeJson jsoninterval <$> obj .: "start" <*> obj .: "end"interval :: forall i. i -> i -> Interval' iinterval s e = Interval { start: s, end: e }data TimeSpan' t= Before t| During (Interval' t)| After ttype TimeSpan = TimeSpan' DateTimederive instance timeSpanFunctor :: Functor TimeSpan'instance timeSpanFoldable :: Foldable TimeSpan' wherefoldr f b = case _ ofBefore a -> f a bDuring x -> foldr f b xAfter a -> f a bfoldl f b = case _ ofBefore a -> f b aDuring x -> foldl f b xAfter a -> f b afoldMap = foldMapDefaultRinstance timeSpanTraversable :: Traversable TimeSpan' wheretraverse f = case _ ofBefore a -> Before <$> f aDuring x -> During <$> traverse f xAfter a -> After <$> f asequence = traverse identityapiLogStart :: ProjectId -> Aff (Either TimelineError Instant)apiLogStart (ProjectId pid) = dolet requestBody = Just <<< RB.Json <<< encodeJson $ { schemaVersion: "2.0" }response <- post RF.json ("/api/user/projects/" <> UUID.toString pid <> "/logStart") requestBodyliftEffect <<< runExceptT $ doevent <- withExceptT LogFailure $ parseDatedResponse responsecase event ofStartEvent t -> pure tStopEvent _ -> throwError <<< Unexpected $ "Expected start event, got stop."apiLogEnd :: ProjectId -> Aff (Either TimelineError Instant)apiLogEnd (ProjectId pid) = dolet requestBody = Just <<< RB.Json <<< encodeJson $ { schemaVersion: "2.0" }response <- post RF.json ("/api/user/projects/" <> UUID.toString pid <> "/logEnd") requestBodyliftEffect <<< runExceptT $ doevent <- withExceptT LogFailure $ parseDatedResponse responsecase event ofStartEvent _ -> throwError <<< Unexpected $ "Expected stop event, got start."StopEvent t -> pure tnewtype ListIntervalsResponse a = ListIntervalsResponse{ workIndex :: Array ({ intervals :: Array a })}derive instance listIntervalsResponseNewtype :: Newtype (ListIntervalsResponse a) _derive instance listIntervalsResponseFunctor :: Functor ListIntervalsResponseinstance listIntervalsResponseFoldable :: Foldable ListIntervalsResponse wherefoldr f b (ListIntervalsResponse r) = foldr f b (r.workIndex >>= _.intervals)foldl f b (ListIntervalsResponse r) = foldl f b (r.workIndex >>= _.intervals)foldMap = foldMapDefaultRinstance listIntervalsResponseTraversable :: Traversable ListIntervalsResponse wheretraverse f (ListIntervalsResponse r) =let traverseCreditRow r' = ({ intervals: _ }) <$> traverse f r'.intervalsin (ListIntervalsResponse <<< ({ workIndex: _ })) <$> traverse traverseCreditRow r.workIndexsequence = traverse identityinstance listIntervalsResponseDecodeJson :: DecodeJson a => DecodeJson (ListIntervalsResponse a) wheredecodeJson = map ListIntervalsResponse <<< decodeJson_ListIntervalsResponse :: Proxy (JsonCompose ListIntervalsResponse Interval' String)_ListIntervalsResponse = ProxyapiListIntervals :: ProjectId -> TimeSpan -> Aff (Either TimelineError (Array Interval))apiListIntervals pid ts = dots' <- liftEffect $ traverse (JD.toISOString <<< JD.fromDateTime) tslet queryElements = case ts' ofBefore t -> ["before=" <> t]During (Interval x) -> ["after=" <> x.start, "before=" <> x.end]After t -> ["after=" <> t]response <- get RF.json ("/api/user/projects/" <> pidStr pid <> "/workIndex?" <> intercalate "&" queryElements)liftEffect<<< runExceptT<<< map (\(ListIntervalsResponse r) -> r.workIndex >>= (_.intervals))<<< map decompose<<< withExceptT LogFailure$ parseDatedResponse response - edit in client/src/Aftok/Timeline.purs at line 6
import Control.Monad.Error.Class (throwError)import Control.Monad.Except.Trans (withExceptT, runExceptT) - edit in client/src/Aftok/Timeline.purs at line 10
import Data.Argonaut.Decode (class DecodeJson, decodeJson, (.:), (.:?)) - replacement in client/src/Aftok/Timeline.purs at line 13
import Data.Either (Either(..), note)import Data.Either (Either(..)) - replacement in client/src/Aftok/Timeline.purs at line 15
import Data.Foldable (class Foldable, any, foldMapDefaultR, intercalate, foldr, foldl, length)import Data.JSDate as JDimport Data.Foldable (any, length) - replacement in client/src/Aftok/Timeline.purs at line 18
import Data.Newtype (class Newtype, unwrap)import Data.Newtype (unwrap) - replacement in client/src/Aftok/Timeline.purs at line 21
import Data.Traversable (class Traversable, traverse_, traverse)import Data.Traversable (traverse_) - edit in client/src/Aftok/Timeline.purs at line 24
import Data.UUID as UUID - edit in client/src/Aftok/Timeline.purs at line 25
import Type.Proxy (Proxy(..)) - edit in client/src/Aftok/Timeline.purs at line 32[2.1291]→[3.3035:3036](∅→∅),[3.301530]→[3.3035:3036](∅→∅),[3.3035]→[3.3035:3036](∅→∅),[3.3036]→[2.1292:1318](∅→∅),[2.1318]→[3.2764:2873](∅→∅),[3.2764]→[3.2764:2873](∅→∅)
import Affjax (get, post)import Affjax.RequestBody as RBimport Affjax.ResponseFormat as RFimport Data.Argonaut.Encode (encodeJson) - edit in client/src/Aftok/Timeline.purs at line 48
import Aftok.Api.Timeline as TLimport Aftok.Api.Timeline (TimelineError, Interval'(..), Interval, TimeSpan) - replacement in client/src/Aftok/Timeline.purs at line 51[3.2907]→[2.1407:1557](∅→∅),[3.599]→[3.3001:3002](∅→∅),[2.1557]→[3.3001:3002](∅→∅),[3.3001]→[3.3001:3002](∅→∅),[3.3002]→[2.1558:1605](∅→∅),[3.633]→[3.3220:3221](∅→∅),[2.1605]→[3.3220:3221](∅→∅),[3.3036]→[3.3220:3221](∅→∅),[3.301927]→[3.3220:3221](∅→∅),[3.3220]→[3.3220:3221](∅→∅),[3.3221]→[2.1606:2534](∅→∅),[2.2534]→[3.3278:3282](∅→∅),[3.3278]→[3.3278:3282](∅→∅),[3.3282]→[2.2535:3080](∅→∅)
import Aftok.Project (Project, Project'(..), ProjectId(..), pidStr)import Aftok.Types (APIError, System, JsonCompose, decompose, parseDatedResponse)data Event' i= StartEvent i| StopEvent itype Event = Event' Instantderive instance eventFunctor :: Functor Event'instance eventFoldable :: Foldable Event' wherefoldr f b = case _ ofStartEvent a -> f a bStopEvent a -> f a bfoldl f b = case _ ofStartEvent a -> f b aStopEvent a -> f b afoldMap = foldMapDefaultRinstance eventTraversable :: Traversable Event' wheretraverse f = case _ ofStartEvent a -> StartEvent <$> f aStopEvent a -> StopEvent <$> f asequence = traverse identityinstance decodeJsonEvent :: DecodeJson (Event' String) wheredecodeJson json = doobj <- decodeJson jsonevent <- obj .: "event"start' <- traverse (_ .: "eventTime") =<< event .:? "start"stop' <- traverse (_ .: "eventTime") =<< event .:? "stop"note "Only 'stop' and 'start' events are supported." $ (StartEvent <$> start') <|> (StopEvent <$> stop')newtype Interval' i = Interval{ start :: i, end :: i}derive instance intervalEq :: (Eq i) => Eq (Interval' i)derive instance intervalNewtype :: Newtype (Interval' i) _type Interval = Interval' Instantderive instance intervalFunctor :: Functor Interval'instance intervalFoldable :: Foldable Interval' wherefoldr f b (Interval i) = f i.start (f i.end b)foldl f b (Interval i) = f (f b i.start) i.endfoldMap = foldMapDefaultRinstance intervalTraversable :: Traversable Interval' wheretraverse f (Interval i) = interval <$> f i.start <*> f i.endsequence = traverse identityimport Aftok.Project (Project, Project'(..), ProjectId)import Aftok.Types (System) - edit in client/src/Aftok/Timeline.purs at line 54
instance decodeJsonInterval :: DecodeJson (Interval' String) wheredecodeJson json = doobj <- decodeJson jsoninterval <$> obj .: "start" <*> obj .: "end"interval :: forall i. i -> i -> Interval' iinterval s e = Interval { start: s, end: e }data TimeSpan' t= Before t| During (Interval' t)| After ttype TimeSpan = TimeSpan' DateTimederive instance timeSpanFunctor :: Functor TimeSpan'instance timeSpanFoldable :: Foldable TimeSpan' wherefoldr f b = case _ ofBefore a -> f a bDuring x -> foldr f b xAfter a -> f a bfoldl f b = case _ ofBefore a -> f b aDuring x -> foldl f b xAfter a -> f b afoldMap = foldMapDefaultRinstance timeSpanTraversable :: Traversable TimeSpan' wheretraverse f = case _ ofBefore a -> Before <$> f aDuring x -> During <$> traverse f xAfter a -> After <$> f asequence = traverse identity - edit in client/src/Aftok/Timeline.purs at line 73[3.3626]→[3.3112:3157](∅→∅),[3.3157]→[2.4057:4079](∅→∅),[2.4079]→[3.634:736](∅→∅),[3.3157]→[3.634:736](∅→∅),[3.736]→[2.4080:4102](∅→∅),[3.736]→[3.3157:3158](∅→∅),[2.4102]→[3.3157:3158](∅→∅),[3.3157]→[3.3157:3158](∅→∅)
data TimelineError= LogFailure (APIError)| Unexpected Stringinstance showTimelineError :: Show TimelineError whereshow = case _ ofLogFailure e -> show eUnexpected t -> t - replacement in client/src/Aftok/Timeline.purs at line 105
{ limits: { bounds: interval bottom bottom, current: bottom }{ limits: { bounds: TL.interval bottom bottom, current: bottom } - replacement in client/src/Aftok/Timeline.purs at line 165
timeSpan <- Before <$> lift system.nowDateTime -- FIXME, should come from a form controltimeSpan <- TL.Before <$> lift system.nowDateTime -- FIXME, should come from a form control - replacement in client/src/Aftok/Timeline.purs at line 203
in interval startInstant (maybe startInstant fromDateTime endOfDay)in TL.interval startInstant (maybe startInstant fromDateTime endOfDay) - replacement in client/src/Aftok/Timeline.purs at line 287
s { active = s.active <|> Just (interval t t)s { active = s.active <|> Just (TL.interval t t) - replacement in client/src/Aftok/Timeline.purs at line 294
(\i -> M.unionWith (<>) (toHistory [interval (unwrap i).start t]) s.history)(\i -> M.unionWith (<>) (toHistory [TL.interval (unwrap i).start t]) s.history) - replacement in client/src/Aftok/Timeline.purs at line 302
, active = map (\(Interval i) -> interval i.start t) s.active, active = map (\(Interval i) -> TL.interval i.start t) s.active - edit in client/src/Aftok/Timeline.purs at line 310[3.6890]→[3.3613:3709](∅→∅),[3.3709]→[3.6092:6171](∅→∅),[3.6092]→[3.6092:6171](∅→∅),[3.6171]→[2.7786:8116](∅→∅),[3.2588]→[3.6622:6623](∅→∅),[2.8116]→[3.6622:6623](∅→∅),[3.6622]→[3.6622:6623](∅→∅),[3.6623]→[3.3710:3802](∅→∅),[3.3802]→[3.6709:6788](∅→∅),[3.6709]→[3.6709:6788](∅→∅),[3.6788]→[2.8117:10282](∅→∅),[3.3240]→[3.7237:7238](∅→∅),[2.10282]→[3.7237:7238](∅→∅),[3.7237]→[3.7237:7238](∅→∅)
apiLogStart :: ProjectId -> Aff (Either TimelineError Instant)apiLogStart (ProjectId pid) = dolet requestBody = Just <<< RB.Json <<< encodeJson $ { schemaVersion: "2.0" }response <- post RF.json ("/api/user/projects/" <> UUID.toString pid <> "/logStart") requestBodyliftEffect <<< runExceptT $ doevent <- withExceptT LogFailure $ parseDatedResponse responsecase event ofStartEvent t -> pure tStopEvent _ -> throwError <<< Unexpected $ "Expected start event, got stop."apiLogEnd :: ProjectId -> Aff (Either TimelineError Instant)apiLogEnd (ProjectId pid) = dolet requestBody = Just <<< RB.Json <<< encodeJson $ { schemaVersion: "2.0" }response <- post RF.json ("/api/user/projects/" <> UUID.toString pid <> "/logEnd") requestBodyliftEffect <<< runExceptT $ doevent <- withExceptT LogFailure $ parseDatedResponse responsecase event ofStartEvent _ -> throwError <<< Unexpected $ "Expected stop event, got start."StopEvent t -> pure tnewtype ListIntervalsResponse a = ListIntervalsResponse{ workIndex :: Array ({ intervals :: Array a })}derive instance listIntervalsResponseNewtype :: Newtype (ListIntervalsResponse a) _derive instance listIntervalsResponseFunctor :: Functor ListIntervalsResponseinstance listIntervalsResponseFoldable :: Foldable ListIntervalsResponse wherefoldr f b (ListIntervalsResponse r) = foldr f b (r.workIndex >>= _.intervals)foldl f b (ListIntervalsResponse r) = foldl f b (r.workIndex >>= _.intervals)foldMap = foldMapDefaultRinstance listIntervalsResponseTraversable :: Traversable ListIntervalsResponse wheretraverse f (ListIntervalsResponse r) =let traverseCreditRow r' = ({ intervals: _ }) <$> traverse f r'.intervalsin (ListIntervalsResponse <<< ({ workIndex: _ })) <$> traverse traverseCreditRow r.workIndexsequence = traverse identityinstance listIntervalsResponseDecodeJson :: DecodeJson a => DecodeJson (ListIntervalsResponse a) wheredecodeJson = map ListIntervalsResponse <<< decodeJson_ListIntervalsResponse :: Proxy (JsonCompose ListIntervalsResponse Interval' String)_ListIntervalsResponse = ProxyapiListIntervals :: ProjectId -> TimeSpan -> Aff (Either TimelineError (Array Interval))apiListIntervals pid ts = dots' <- liftEffect $ traverse (JD.toISOString <<< JD.fromDateTime) tslet queryElements = case ts' ofBefore t -> ["before=" <> t]During (Interval x) -> ["after=" <> x.start, "before=" <> x.end]After t -> ["after=" <> t]response <- get RF.json ("/api/user/projects/" <> pidStr pid <> "/workIndex?" <> intercalate "&" queryElements)liftEffect<<< runExceptT<<< map (\(ListIntervalsResponse r) -> r.workIndex >>= (_.intervals))<<< map decompose<<< withExceptT LogFailure$ parseDatedResponse response - replacement in client/src/Aftok/Timeline.purs at line 313
, logStart: apiLogStart, logEnd: apiLogEnd, listIntervals: apiListIntervals, logStart: TL.apiLogStart, logEnd: TL.apiLogEnd, listIntervals: TL.apiListIntervals - replacement in lib/Aftok/Database/PostgreSQL.hs at line 400[3.1002]→[3.1002:1102](∅→∅),[3.1102]→[3.11913:12004](∅→∅),[3.20358]→[3.11913:12004](∅→∅),[3.11913]→[3.11913:12004](∅→∅),[3.12004]→[3.16287:16361](∅→∅),[3.16287]→[3.16287:16361](∅→∅)
credit_to_network, credit_to_address, credit_to_user_id, credit_to_project_id,event_type, event_time, event_metadataFROM work_eventsWHERE project_id = ? AND user_id = ? AND event_time >= ? |]credit_to_network, credit_to_address, credit_to_user_id, credit_to_project_id,event_type, event_time, event_metadataFROM work_eventsWHERE project_id = ? AND user_id = ? AND event_time >= ? |] - edit in lib/Aftok/Database/PostgreSQL.hs at line 406
pgEval (FindLatestEvents (ProjectId pid) (UserId uid) i) = domode <- askNetworkModepquery(logEntryParser mode)[sql| SELECT credit_to_type,credit_to_network, credit_to_address, credit_to_user_id, credit_to_project_id,event_type, event_time, event_metadataFROM work_eventsWHERE project_id = ?AND user_id = ?ORDER BY event_time DESCLIMIT ?|](pid, uid, i) - edit in lib/Aftok/Database.hs at line 61
FindLatestEvents ::ProjectId -> UserId -> Int -> DBOp [LogEntry BTCNet] - edit in lib/Aftok/Database.hs at line 224
findLatestEvents :: (MonadDB m) => ProjectId -> UserId -> Int -> m [LogEntry BTCNet]findLatestEvents p u i = liftdb $ FindLatestEvents p u i - edit in server/Aftok/Snaplet/Util.hs at line 2
- edit in server/Aftok/Snaplet/Util.hs at line 3
import Data.Attoparsec.ByteString (parseOnly)import Data.Attoparsec.ByteString.Char8 (decimal) - edit in server/Aftok/Snaplet/Util.hs at line 19[3.4619]
decimalParam :: (Integral i, MonadSnap m) => ByteString -> m (Maybe i)decimalParam k = runMaybeT $ dobs <- MaybeT $ getParam kMaybeT . pure . either (const Nothing) Just $ parseOnly decimal bs - edit in server/Aftok/Snaplet/WorkLog.hs at line 101
userLatestEntries :: S.Handler App App [LogEntry (NetworkId, Address)]userLatestEntries = douid <- requireUserIdpid <- requireProjectIdlimit <- fromMaybe 1 <$> decimalParam "limit"snapEval $ findLatestEvents pid uid limit - edit in server/Main.hs at line 75
projectPayoutsRoute =serveJSON (payoutsJSON nmode) $ method GET payoutsHandler - edit in server/Main.hs at line 78[3.2550]→[2.15344:15428](∅→∅),[3.2613]→[3.2569:2570](∅→∅),[3.2979]→[3.2569:2570](∅→∅),[3.3485]→[3.2569:2570](∅→∅),[3.7960]→[3.2569:2570](∅→∅),[2.15428]→[3.2569:2570](∅→∅),[3.26983]→[3.2569:2570](∅→∅),[3.39611]→[3.2569:2570](∅→∅),[3.63767]→[3.2569:2570](∅→∅),[3.2569]→[3.2569:2570](∅→∅)
projectPayoutsRoute = serveJSON (payoutsJSON nmode) $ method GET payoutsHandler - replacement in server/Main.hs at line 91
auctionRoute = serveJSON auctionJSON $ method GET auctionGetHandlerauctionBidRoute = serveJSON bidIdJSON $ method POST auctionBidHandlerauctionRoute =serveJSON auctionJSON $ method GET auctionGetHandlerauctionBidRoute =serveJSON bidIdJSON $ method POST auctionBidHandler