This also substantially cleans up JSON serialization in the APIs, and also adds a Docker build for the client.
IR75ZMX32SFFMDNV2I2L22X5JTWCOC4UUBCSPU7S6VHR6HFV6ADQC 5SBSBFLSDRLVSWPBVP4SWOKNAXWIZL5YR646VFCBRUAG2C5QOGUQC 4354Y4PECM6BOEYIKW2L6WP6ULDIQK2KMNLORWPVKHKQTHUI6CRQC MU6WOCCJQWG4A5NLD3GBFATCE3SRE3QQCYXYH6WIKSGLHQOOBVRAC B6HWAPDPXIWH7CHK5VLMWLL6EQN6NOFZEFYO47BPUY2ZO4SL7VDAC SOIAMXLWIB5RIEMKXUFMBSE2SKQQTMHYSW3DKUX6GEV4VNOQVHAQC LEINLS3X55PB6TSCNC5RVMDMV56XHTV4MNDUC42H7DDFMPDYUNTAC AWWC6P5ZVFDQHX3EAYDG4DKTUZ6A5LHQAV3NIUO3VP6FM7JKPK5AC IPG33FAWXGEQ2PO6OXRT2PWWXHRNMPVUKKADL6UKKN5GD2CNZ25AC AL37SVTCKRSG4HG2PCYK5Z7QSIZZH5JHH4Q2VLMXFAXSAQRFFG4QC 64C6AWH66FDKU6UE6Z6JPX2J2GBM2JOPTH2GL6LHKAIUBGNGDZ5AC RB2ETNIFLQUA6OA66DAEOXZ25ENMQGNKX5CZRSKEYHTD6BQ6NTFQC 2J37EVJMX255K3XEJHTZGRPEIRMAQ62JQWOA7JU3YTZUB6PUPWVQC RSF6UAJKG7CEKILSVXI6C4YZXY7PIYZM2EMA2IXKQ7SADKNVSH7QC OUR4PAOTXXKXQPMAR5TIYX7MBRRJS2WVTZS7SN4SOGML7SPJIJGQC JXG3FCXYBDKMUD77DOM7RCIJYKB7BILC43OHHDZBE7YQRGAMUCCAC QMEYU4MWLTSWPWEEOFRLK2IKE64BY3V5X73323WPLCGPP3TPDYGAC QU5FW67RGCWOWT2YFM4NYMJFFHWIRPQANQBBAHBKZUY7UYMCSIMQC ZIG57EE6RB22JGB3CT33EN2HVYCHCXBT5GROBTBMBLEMDTGQOOBQC NJNMO72S7VIUV22JXB4IFPZMHWTJAOTP6EC6Z4QSKYIROSXT52MQC BFZN4SUAGYNFFYVAP36BAX32DMO622PK4EPEVQQEAGC2IHTEAAPQC WRPIYG3EUHZR6N6T74ZXZDXATRMIRLXAQ24UNUNSVTVYGMT2VDSQC J6S23MDGHVSCVVIRB6XRNSY3EGTDNWFJHV7RYLIEHBUK5KU63CFQC 4ZLEDBK7VGLKFUPENAFLUJYNFLKFYJ3TREPQ7P6PKMYGJUXB55HQC GMYPBCWEB6NKURRILAHR3TJUKDOGR2ZMK5I6MS6P5G2LAGH36P3QC MJDIMD5BQEBC265AQAGYE2K6EHHS7ZMZY3I6WE5MCDSTA2E2VY7AC SQ7UMLN5WCPHIF66RO4UQVX6RSNRRZBOVZP7HEMSKP7VO6YNQPRAC NSRSSSTRMJPPUYQANYDWGI5D3NVM6RQEVZCDUUNQAOL3OWQTD27AC M4PWY5RUV72AEDCNC4O7UKBPHBIACR4354YTSC3SUZGWFV5UBJBQC JUFBTX45TKSZMB2D4CGNB73UYM5FXAV2QMKIHBSMHEQDAECYP7HQC IZEVQF627FA7VV25KJAWYWGGC35LZUUBBQRPN5ZAINTQLPEDRTEAC X3ES7NUA42D2BF7CQDDKXM5CLMVCYA3H5YU5KXLPTGDBFPE2LNVAC QMRKFEPGFBCEWAIXPEIG5ILKAJ2JH5L3TOITHR4HNJXK5FN3KXBQC EFSXYZPOGA5M4DN65IEIDO7JK6U34DMQELAPHOIL2UAT6HRC66BAC 2XQD6KKKD6QVHFHAEMVE3XXY7X2T7BLTLL7XIILZAXNJJH2YONUQC O5FVTOM6YFBLEPF3S576K6IMT6ZZ5VQCSB3YVXNS4CKBITKCRZ7AC HMDM3B557TO5RYP2IGFFC2C2VN6HYZTDQ47CJY2O37BW55DSMFZAC EQXRXRZDYCM7BDAVBOXQYPG6C7IJT3OFGNIXCDAHJJBRKAXNGL7AC Z7KS5XHHC6PAMTVBHXY7KUSS3BWAOU6FSYIITUCFOOJZU4OUJHBAC 4R7XIYK3BP664CO3YJ2VM64ES2JYN27UTQG5KS34OTEPAIODSZLQC 7XN3I3QJHYMKU2DCUXX34WQMSJ4ZJOWW7FME34EANO3E5W4Q632AC 7KZP4RHZ3QSYTPPQ257A65Z5UPX44TF2LAI2U5EMULQCLDCEUK2AC 7DBNV3GV773FH5ZLQWFX4RBOS4Q3CIK2RYZNNABY3ZOETYZCXRNQC RPAJLHMTUJU4AYNBOHVGHGGB4NY2NLY3BVPYN5FMWB3ZIMAUQHCQC SCXG6TJWYIPRUMT27KGKIIF6FYKTUTY74UNZ2FQTT63XZ6HIF3AAC 7HPY3QPFPN35PSPUBVNW2GTFB3CBQZBST4J2BAVJ7QMXLIUN52JAC N4NDAZYTLSI2W22KT3SYXL257DBMSH3UT2BXOYM7LH7FSZAY4RLAC A6HKMINBNGQLLX4QJMYWKQ4JAEHVJ4HIRVDKPPDI3FJUO2AAB7OQC W35DDBFYF6Z4ZPCFEO5RPAONZLYCSNTXUSTS6FIUUVZHCI6Q7GHAC EW2XN7KUMCAQNVFJJ5YTAVDZCPHNWDOEDMRFBUGLY6IE2HKNNX5AC XTBSG4C7SCZUFOU2BTNFR6B6TCGYI35BWUV4PVTS3N7KNH5VEARQC 4IQVQL4TS35GL2GYZJG254TKJLL5EHMRSFT77Z4VTRZIG2TMBM3QC GCVQD44VRPQVKPZEPIC4AOIXLJIG2ZMV3QI2Y7KALUT6NVUBSGSAC UWMGUJOW5X5HQTS76T2FD7MNAJF7SESPQVU5FDIZO52V75TT2X6AC 7VGYLTMURLVSVUYFW7TCRZTDQ6RE2EPSPPA43XKHDOBFWYVVSJHQC ASF3UPJLCX7KIUCNJD5KAXSPDXCUEJHLL4HBRTRUBPCW73IXOCWQC Z3MK2PJ5U222DXRS22WCDHVPZ7HVAR3HOCUNXIGX6VMEPBQDF6PQC POX3UAMTGCNS3SU5I6IKDNCCSHEAUSFBZ3WCMQ3EXKPNXETOI6BAC RN7EI6INGUUHGMNY5RU3NH56WPLRZY5ZMYDNFMNE3TGV4ESFLQIAC O722AOKEWXWJPRHGJREU6QPW7HEFPPRETZIAADZ2RMAXHARCNEKAC O227CEAV7BTKSE3SSC7XHC5IWEBXZL2AOOKJMBMOOFNTLINBLQMAC WAIX6AGNDVJOKTWZ7OP7QOYSJHAJSX5EOWXZHOAO2IG6ALWUCJ6QC DFOBMSAODB3NKW37B272ZXA2ML5HMIH3N3C4GT2DPEQS7ZFK4SNAC Q5X5RYQLP5K7REYD6VLHOKC4W36ZELJYA45V6YFKTD5S6MPN3NDQC UILI6PILCRDPZ3XYA54LGIGPSU7ERWNHCE7R3CE64ZEC7ONOEMOQC BWN72T44GRRZ6K2OPN56FTLNEB7J7AGC7T2U5HSMLEKUPGJP2NUAC NVOCQVASZWTKQJG7GPH7KHKZZR7NUG4WLV5YY4KAIRPCJRWCZPIAC TLQ72DSJD7GGPWN6HGBHAVPBRQFKEQ6KSK43U7JWWID4ZWAF47JAC GKGVYBZGPJXO7N7GLHLRNYQPXFHBQSNQN53OKRFCXLQEYDTC5I4QC 5DRIWGLUKMQZU2ZPBXSTLAWJKAMOD5YXAHM5LEDQHDFGYYLHWCDQC NEDDHXUK3GNFMOFO3KLU7NRIHCTYNWBT3D6HTKZAOXFDE6HMDZ6AC 4QX5E5ACVN57KJLCWOM4JEI6JSV4XZNCWVYPOTKSOMUW3SOMCNJAC U256ZALIPTVWLNACYPIMWLNEYDQWP7CHF4Y4CGMILQTONJHMGQVQC KNSI575VAW6HRCZYXOEPQ4DTSML4EORML5MV4DJBRKE7TXCPS4EAC Y35QCWYW2OTZ27ZVTH2BA3XJTUCJ2WMLKU32ZCOCDY3AW7TIZXRAC A2J7B4SCCJYKQV3G2LDHEFNE2GUICO3N3Y5FKF4EUZW5AG7PTDWAC 2WOOGXDHVQ6L2MQYUTLJ6H6FVSQNJN6SMJL5DG7HAHFYPJLRT2SAC PBD7LZYQHXAA3KLH2ZUX5GW4UFML6BQ32KXZF4KZ6OYFASUYFJ5QC HALRDT2F22DAQ54M334YSKD2XJCAXDXEZPDKGMOWKH7XELBAS6MAC 4U7F3CPIDTK6JSEDMNMHVKSR7HOQDLZQD2PPVMDLHO5SFSIMUXZAC BROSTG5KP3NUNLSYPVQID254TE47E5RKQAKLPIY7PGWETE6JNMTAC I2KHGVD44KT4MQJXGCTVSQKMBO6TVCY72F26TLXGWRL6PHGF6RNQC Z24SZOGZJLDTDTGWH7M25RYQ7MYSU52ZLFWJ2PSQFTMK4J35PIWAC EMVTF2IWNQGRL44FC4JNG5FYYQTZSFPNM6SOM7IAEH6T7PPK2NVAC ADMKQQGCGVSHHIMVQ4XFRDCG544SBJCYALSKZV45CQQBZ4ACUH2AC docker tag aftok/aftok-client:latest aftok/aftok-client:$(VERSION)docker push docker.io/aftok/aftok-client:latestdocker push docker.io/aftok/aftok-client:$(VERSION)
FROM ubuntu:focalMAINTAINER Kris Nuttycombe <kris@aftok.com>ENV LANG C.UTF-8ENV TZ America/DenverRUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone# Install build tools & library dependenciesRUN apt-get update && \apt-get install -y --no-install-recommends \libtinfo5 nodejs npm netbaseRUN apt-get install -y --no-install-recommends ca-certificatesRUN update-ca-certificatesRUN mkdir -p /opt/aftok/clientWORKDIR /opt/aftok/clientADD ./client/package.json /opt/aftok/client/package.jsonRUN npm installENV PATH="./node_modules/.bin:${PATH}"# Add static assetsADD ./aftok.com/src/assets /opt/aftok/aftok.com/src/assetsADD ./client/dev /opt/aftok/client/devRUN mkdir -p /opt/aftok/client/prod && \ln -s /opt/aftok/aftok.com/src/assets /opt/aftok/client/prod/assets# Add purescript build config & sourcesADD ./client/spago.dhall /opt/aftok/client/spago.dhallADD ./client/packages.dhall /opt/aftok/client/packages.dhallADD ./client/src /opt/aftok/client/srcRUN npm run build-prod# Add dist-volume directory for use with docker-compose sharing# of client executables via volumes.ADD ./docker/aftok-client-cp.sh /opt/aftok/RUN chmod 700 /opt/aftok/aftok-client-cp.shRUN mkdir /opt/aftok/client/dist-volume
instance decodeJsonEvent :: DecodeJson (Event' String) where
parseEventFields :: Object Json -> Either String (Event String)parseEventFields obj = doev <- obj .: "event"start' <- traverse (_ .: "eventTime") =<< ev .:? "start"stop' <- traverse (_ .: "eventTime") =<< ev .:? "stop"note "Only 'stop' and 'start' events are supported." $(StartEvent <$> start') <|>(StopEvent <$> stop')instance eventDecodeJSON :: DecodeJson (Event String) wheredecodeJson = parseEventFields <=< decodeJsonnewtype KeyedEvent i = KeyedEvent{ eventId :: String, event :: Event i}keyedEvent :: forall i. String -> Event i -> KeyedEvent ikeyedEvent eid ev = KeyedEvent { eventId: eid, event: ev }eventId :: forall i. KeyedEvent i -> StringeventId (KeyedEvent xs) = xs.eventIdevent :: forall i. KeyedEvent i -> Event ievent (KeyedEvent xs) = xs.eventderive instance keyedEventFunctor :: Functor KeyedEventinstance keyedEventFoldable :: Foldable KeyedEvent wherefoldr f b = foldr f b <<< eventfoldl f b = foldl f b <<< eventfoldMap = foldMapDefaultRinstance keyedEventTraversable :: Traversable KeyedEvent wheretraverse f (KeyedEvent xs) = (\ev -> KeyedEvent { eventId: xs.eventId, event: ev }) <$> traverse f xs.eventsequence = traverse identityinstance keyedEventDecodeJson :: DecodeJson (KeyedEvent String) where
event <- obj .: "event"start' <- traverse (_ .: "eventTime") =<< event .:? "start"stop' <- traverse (_ .: "eventTime") =<< event .:? "stop"note "Only 'stop' and 'start' events are supported." $ (StartEvent <$> start') <|> (StopEvent <$> stop')
keyedEvent <$> obj .: "eventId" <*> parseEventFields obj
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.end
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.end
event <- withExceptT LogFailure $ parseDatedResponse responsecase event ofStartEvent t -> pure tStopEvent _ -> throwError <<< Unexpected $ "Expected start event, got stop."
kev <- withExceptT LogFailure $ parseDatedResponse responsecase event kev ofStartEvent _ -> pure kevStopEvent _ -> throwError <<< Unexpected $ "Expected start event, got stop."
import Aftok.Api.Timeline (TimelineError, Interval'(..), Interval, TimeSpan, start, end, interval)
import Aftok.Api.Timeline( TimelineError,Event(..),Interval(..),TimeInterval,KeyedEvent,TimeSpan,start, end, interval,event, eventTime, keyedEvent)
data TimelineEvent= LoggedEvent (KeyedEvent Instant)| PhantomEvent Instantinstance showTimelineEvent :: Show TimelineEvent whereshow = case _ ofLoggedEvent kev -> "Real event at " <> show (event kev)PhantomEvent i -> "Phantom at " <> show itlEventTime :: TimelineEvent -> InstanttlEventTime = case _ ofLoggedEvent kev -> eventTime <<< event $ kevPhantomEvent i -> i
, logStart :: ProjectId -> m (Either TimelineError Instant), logEnd :: ProjectId -> m (Either TimelineError Instant), listIntervals :: ProjectId -> TimeSpan -> m (Either TimelineError (Array Interval)), getLatestEvent :: ProjectId -> m (Either TimelineError (Maybe TL.Event))
, logStart :: ProjectId -> m (Either TimelineError (KeyedEvent Instant)), logEnd :: ProjectId -> m (Either TimelineError (KeyedEvent Instant)), listIntervals :: ProjectId -> TimeSpan -> m (Either TimelineError (Array (Interval (KeyedEvent Instant)))), getLatestEvent :: ProjectId -> m (Either TimelineError (Maybe (KeyedEvent Instant)))
updateStart :: Instant -> TimelineState -> TimelineStateupdateStart t s =s { active = s.active <|> Just (TL.interval t t) }
updateStart :: KeyedEvent Instant -> TimelineState -> TimelineStateupdateStart ev s =s { active = s.active <|> Just (TL.interval (LoggedEvent ev) (PhantomEvent <<< eventTime <<< event $ ev)) }
updateStop system t st = donewHistory <- join <$> traverse (\i -> runMaybeT $ toHistory system [TL.interval (start i) t]) st.active
updateStop system ev st = dolet updateHistory i = runMaybeT $ toHistory system [TL.interval (start i) (LoggedEvent ev)]newHistory <- join <$> traverse updateHistory st.active
, logStart: \_ -> Right <$> liftEffect now, logEnd: \_ -> Right <$> liftEffect now
, logStart: \_ -> Right <<< keyedEvent "" <<< StartEvent <$> liftEffect now, logEnd: \_ -> Right <<< keyedEvent "" <<< StopEvent <$> liftEffect now
end <- MaybeT <<< pure $ fromDateTime <$> DT.adjust (Days 1.0) (toDateTime start)
nextNoon <- MaybeT <<< pure $fromDateTime <$> (DT.adjust (Hours 12.0) <=< DT.adjust (Days 1.0) $(toDateTime start))Tuple _ end <- MaybeT $ system.dateFFI.midnightLocal nextNoon
--lift <<< system.log $ "Splitting interval " <> show idayBounds@(Tuple date bounds) <- localDayBounds system (start i)split <- if end i < (end bounds)then dopure [Tuple date { dayBounds: bounds, loggedIntervals: [i] }]else dolet firstFragment = [ Tuple date { dayBounds: bounds, loggedIntervals: [interval (start i) (end bounds)]} ]append firstFragment <$> splitInterval system (interval (end bounds) (end i))
lift <<< system.log $ "Splitting interval " <> show i-- day bounds are based on the start event.Tuple date bounds <- localDayBounds system (tlEventTime $ start i)lift <<< system.log $ "Splitting on day bounds: " <> show (start bounds) <> " to " <> show (end bounds)split <- if tlEventTime (end i) < end boundsthen dolift <<< system.log $ "Split complete"pure [Tuple date { dayBounds: bounds, loggedIntervals: [i] }]else dolet splitEvent = PhantomEvent (end bounds)currInterval = Tuple date { dayBounds: bounds, loggedIntervals: [interval (start i) splitEvent] }nextInterval = interval splitEvent (end i)lift <<< system.log $ "Split required; first fragment: " <> show currInterval <> "; next interval: " <> show nextIntervalcons currInterval <$> splitInterval system nextInterval
#!/bin/bashecho "Copying client build artifacts to mounted volume..."cp -r /opt/aftok/client/dist/* /opt/aftok/client/dist-volumeecho "Client copy complete. The container will now shut down."
zcashd:image: electriccoinco/zcashd:v4.0.0container_name: aftok-zcashdexpose:- "18232"ports:- "18233:18233"volumes:- type: bindsource: ./local/zcashd/zcash-datatarget: /srv/zcashd/.zcash- type: bindsource: ./local/zcashd/zcash-paramstarget: /srv/zcashd/.zcash-paramsread_only: truenetworks:- back-tier
# zcashd:# image: electriccoinco/zcashd:v4.0.0# container_name: aftok-zcashd# expose:# - "18232"# ports:# - "18233:18233"# volumes:# - type: bind# source: ./local/zcashd/zcash-data# target: /srv/zcashd/.zcash# - type: bind# source: ./local/zcashd/zcash-params# target: /srv/zcashd/.zcash-params# read_only: true# networks:# - back-tier
logEntryParser[sql| SELECT credit_to_type,credit_to_account, credit_to_user_id, credit_to_project_id,event_type, event_time,event_metadataFROM work_eventsWHERE project_id = ? AND user_id = ? AND event_time <= ?ORDER BY event_time DESCLIMIT ?|]
keyedLogEntryParser[sql| SELECT id, credit_to_type,credit_to_account, credit_to_user_id, credit_to_project_id,event_type, event_time,event_metadataFROM work_eventsWHERE project_id = ? AND user_id = ? AND event_time <= ?AND replacement_id IS NULLORDER BY event_time DESCLIMIT ?|]
logEntryParser[sql| SELECT credit_to_type,credit_to_account, credit_to_user_id, credit_to_project_id,event_type, event_time, event_metadataFROM work_eventsWHERE project_id = ? AND user_id = ?AND event_time >= ? AND event_time <= ?ORDER BY event_time DESCLIMIT ?|]
keyedLogEntryParser[sql| SELECT id, credit_to_type,credit_to_account, credit_to_user_id, credit_to_project_id,event_type, event_time, event_metadataFROM work_eventsWHERE project_id = ? AND user_id = ?AND replacement_id IS NULLAND event_time >= ? AND event_time <= ?ORDER BY event_time DESCLIMIT ?|]
logEntryParser[sql| SELECT credit_to_type,credit_to_account, credit_to_user_id, credit_to_project_id,event_type, event_time, event_metadataFROM work_eventsWHERE project_id = ? AND user_id = ? AND event_time >= ?ORDER BY event_time DESCLIMIT ?|]
keyedLogEntryParser[sql| SELECT id, credit_to_type,credit_to_account, credit_to_user_id, credit_to_project_id,event_type, event_time, event_metadataFROM work_eventsWHERE project_id = ? AND user_id = ? AND event_time >= ?AND replacement_id IS NULLORDER BY event_time DESCLIMIT ?|]
logEntryParser[sql| SELECT credit_to_type,credit_to_account, 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 ?|]
keyedLogEntryParser[sql| SELECT id, credit_to_type,credit_to_account, credit_to_user_id, credit_to_project_id,event_type, event_time, event_metadataFROM work_eventsWHERE project_id = ? AND user_id = ?AND replacement_id IS NULLORDER BY event_time DESCLIMIT ?|]
amendEvent :: EventId -> EventAmendment -> DBM AmendmentIdamendEvent (EventId eid) = \case(TimeChange mt t) ->pinsertAmendmentId[sql| INSERT INTO event_time_amendments(event_id, amended_at, event_time)VALUES (?, ?, ?) RETURNING id |](eid, fromThyme $ mt ^. _ModTime, fromThyme t)(CreditToChange mt c@(CreditToAccount acctId)) ->pinsertAmendmentId[sql| INSERT INTO event_credit_to_amendments(event_id, amended_at, credit_to_type, credit_to_account)VALUES (?, ?, ?, ?) RETURNING id |](eid, fromThyme $ mt ^. _ModTime, creditToName c, acctId ^. _AccountId)(CreditToChange mt c@(CreditToProject pid)) ->pinsertAmendmentId[sql| INSERT INTO event_credit_to_amendments(event_id, amended_at, credit_to_type, credit_to_project_id)VALUES (?, ?, ?, ?) RETURNING id |](eid, fromThyme $ mt ^. _ModTime, creditToName c, pid ^. _ProjectId)(CreditToChange mt c@(CreditToUser uid)) ->pinsertAmendmentId[sql| INSERT INTO event_credit_to_amendments(event_id, amended_at, credit_to_type, credit_to_user_id)VALUES (?, ?, ?, ?) RETURNING id |](eid, fromThyme $ mt ^. _ModTime, creditToName c, uid ^. _UserId)(MetadataChange mt v) ->pinsertAmendmentId[sql| INSERT INTO event_metadata_amendments(event_id, amended_at, event_metadata)VALUES (?, ?, ?) RETURNING id |](eid, fromThyme $ mt ^. _ModTime, v)
pure $ workIndex logEntries
pure $ workIndex (view logEntry) logEntriesamendEvent :: ProjectId -> UserId -> KeyedLogEntry -> EventAmendment -> DBM (EventId, AmendmentId)amendEvent pid uid kle amendment = ptransact $ do(amendId, replacement, amend_t :: Text) <- amendnewEventId <- createEvent pid uid (replacement ^. logEntry)void $pexec[sql| UPDATE work_eventsSET replacement_id = ?, amended_by_id = ?, amended_by_type = ?WHERE id = ? |](newEventId ^. _EventId, amendId ^. _AmendmentId, amend_t, kle ^. workId . _EventId)pure (newEventId, amendId)whereamend = case amendment of(TimeChange mt t) -> doaid <-pinsertAmendmentId[sql| INSERT INTO event_time_amendments(work_event_id, amended_at, event_time)VALUES (?, ?, ?) RETURNING id |](kle ^. workId . _EventId, fromThyme $ mt ^. _ModTime, fromThyme t)pure (aid, set (logEntry . event . eventTime) t kle, "amend_event_time")(CreditToChange mt c@(CreditToAccount acctId)) -> doaid <-pinsertAmendmentId[sql| INSERT INTO event_credit_to_amendments(work_event_id, amended_at, credit_to_type, credit_to_account)VALUES (?, ?, ?, ?) RETURNING id |](kle ^. workId . _EventId, fromThyme $ mt ^. _ModTime, creditToName c, acctId ^. _AccountId)pure (aid, set (logEntry . creditTo) c kle, "amend_credit_to")(CreditToChange mt c@(CreditToProject cpid)) -> doaid <-pinsertAmendmentId[sql| INSERT INTO event_credit_to_amendments(work_event_id, amended_at, credit_to_type, credit_to_project_id)VALUES (?, ?, ?, ?) RETURNING id |](kle ^. workId . _EventId, fromThyme $ mt ^. _ModTime, creditToName c, cpid ^. _ProjectId)pure (aid, set (logEntry . creditTo) c kle, "amend_credit_to")(CreditToChange mt c@(CreditToUser cuid)) -> doaid <-pinsertAmendmentId[sql| INSERT INTO event_credit_to_amendments(work_event_id, amended_at, credit_to_type, credit_to_user_id)VALUES (?, ?, ?, ?) RETURNING id |](kle ^. workId . _EventId, fromThyme $ mt ^. _ModTime, creditToName c, cuid ^. _UserId)pure (aid, set (logEntry . creditTo) c kle, "amend_credit_to")(MetadataChange mt v) -> doaid <-pinsertAmendmentId[sql| INSERT INTO event_metadata_amendments(work_event_id, amended_at, event_metadata)VALUES (?, ?, ?) RETURNING id |](kle ^. workId . _EventId, fromThyme $ mt ^. _ModTime, v)pure (aid, set (logEntry . eventMeta) (Just v) kle, "amend_metadata")
AmendEvent :: EventId -> EventAmendment -> DBOp AmendmentIdFindEvent :: EventId -> DBOp (Maybe KeyedLogEntry)FindEvents :: ProjectId -> UserId -> RangeQuery -> Limit -> DBOp [LogEntry]ReadWorkIndex :: ProjectId -> DBOp WorkIndex
AmendEvent :: ProjectId -> UserId -> KeyedLogEntry -> EventAmendment -> DBOp (EventId, AmendmentId)FindEvent :: EventId -> DBOp (Maybe (ProjectId, UserId, KeyedLogEntry))FindEvents :: ProjectId -> UserId -> RangeQuery -> Limit -> DBOp [KeyedLogEntry]ReadWorkIndex :: ProjectId -> DBOp (WorkIndex KeyedLogEntry)
ev <- findEvent eidlet act = AmendEvent eid aforbidden = raiseOpForbidden uid UserNotEventLogger actmissing = raiseSubjectNotFound actmaybemissing(\(_, uid', _) -> if uid' == uid then liftdb act else forbidden)ev
evMay <- findEvent eidmaybe missing saveAmendment evMaywheremissing = raiseSubjectNotFound (FindEvent eid)saveAmendment (pid, uid', le) =let act = AmendEvent pid uid le ain if uid' == uidthen liftdb actelse raiseOpForbidden uid UserNotEventLogger act
idJSON :: forall a. Text -> Getter a UUID -> a -> ValueidJSON t l a = v1 $ obj [t .= idValue l a]qdbJSON :: Text -> Getter a UUID -> Getter a Value -> a -> ValueqdbJSON name _id _value x =v1 $ obj [(name <> "Id") .= idValue _id x, name .= (x ^. _value)]projectIdJSON :: ProjectId -> ValueprojectIdJSON = idJSON "projectId" _ProjectIdprojectJSON :: P.Project -> ValueprojectJSON p =v1 $obj[ "projectName" .= (p ^. P.projectName),"inceptionDate" .= (p ^. P.inceptionDate),"initiator" .= (p ^. P.initiator . _UserId)]qdbProjectJSON :: (ProjectId, P.Project) -> ValueqdbProjectJSON = qdbJSON "project" (_1 . _ProjectId) (_2 . L.to projectJSON)auctionIdJSON :: A.AuctionId -> ValueauctionIdJSON = idJSON "auctionId" A._AuctionIdauctionJSON :: A.Auction Amount -> ValueauctionJSON x =v1 $obj[ "projectId" .= idValue (A.projectId . _ProjectId) x,"initiator" .= idValue (A.initiator . _UserId) x,"name" .= (x ^. A.name),"description" .= (x ^. A.description),"raiseAmount" .= (x ^. (A.raiseAmount . to amountJSON)),"auctionStart" .= (x ^. A.auctionStart),"auctionEnd" .= (x ^. A.auctionEnd)]
bidIdJSON :: A.BidId -> ValuebidIdJSON pid = v1 $ obj ["bidId" .= (pid ^. A._BidId)]
identifiedJSON :: Text -> Getter a UUID -> Getter a Value -> a -> ValueidentifiedJSON name _id _value x =object [(name <> "Id") .= idValue _id x, name .= (x ^. _value)]
v2 $ obj ["creditToProject" .= projectIdJSON pid]parseCreditTo :: Value -> Parser CreditToparseCreditTo = unversion "CreditTo" $ \case(Version 2 0) -> parseCreditToV2ver -> badVersion "EventAmendment" ver
object ["creditToProject" .= idValue _ProjectId pid]
-- Payouts--payoutsJSON :: FractionalPayouts -> ValuepayoutsJSON (Payouts m) =v2 $let payoutsRec :: (CreditTo, Rational) -> ValuepayoutsRec (c, r) =object ["creditTo" .= creditToJSON c, "payoutRatio" .= r, "payoutPercentage" .= (fromRational @Double r * 100)]in obj $ ["payouts" .= fmap payoutsRec (MS.assocs m)]parsePayoutsJSON :: Value -> Parser FractionalPayoutsparsePayoutsJSON = unversion "Payouts" $ pwherep :: Version -> Object -> Parser FractionalPayoutsp (Version 2 0) val =let parsePayoutRecord x =(,)<$> (parseCreditToV2 =<< (x .: "creditTo"))<*> (x .: "payoutRatio")in Payouts. MS.fromList<$> (traverse parsePayoutRecord =<< parseJSON (Object val))p ver x = badVersion "Payouts" ver x--
workIndexJSON :: WorkIndex -> ValueworkIndexJSON (WorkIndex widx) =v2 $obj ["workIndex" .= fmap widxRec (MS.assocs widx)]wherewidxRec :: (CreditTo, NonEmpty Interval) -> ValuewidxRec (c, l) =object[ "creditTo" .= creditToJSON c,"intervals" .= (intervalJSON <$> L.toList l)]eventIdJSON :: EventId -> ValueeventIdJSON = idJSON "eventId" _EventIdlogEventJSON' :: LogEvent -> ValuelogEventJSON' ev =object [eventName ev .= object ["eventTime" .= (ev ^. eventTime)]]logEntryJSON :: LogEntry -> ValuelogEntryJSON le = v2 $ obj (logEntryFields le)logEntryFields :: LogEntry -> [Pair]logEntryFields (LogEntry c ev m) =[ "creditTo" .= creditToJSON c,"event" .= logEventJSON' ev,"eventMeta" .= m]amendmentIdJSON :: AmendmentId -> ValueamendmentIdJSON = idJSON "amendmentId" _AmendmentId
createSubscriptionJSON :: UserId -> B.BillableId -> Day -> ValuecreateSubscriptionJSON uid bid d =v1 $obj[ "user_id" .= idValue _UserId uid,"billable_id" .= idValue B._BillableId bid,"start_date" .= showGregorian d]subscriptionJSON :: B.Subscription -> ValuesubscriptionJSON = v1 . obj . subscriptionKVsubscriptionKV :: (KeyValue kv) => B.Subscription -> [kv]subscriptionKV sub =[ "user_id" .= idValue (B.customer . _UserId) sub,"billable_id" .= idValue (B.billable . B._BillableId) sub,"start_time" .= view B.startTime sub,"end_time" .= view B.endTime sub]subscriptionIdJSON :: B.SubscriptionId -> ValuesubscriptionIdJSON = idJSON "subscriptionId" B._SubscriptionId-- paymentRequestDetailsJSON :: [PaymentRequestDetail Amount] -> Value-- paymentRequestDetailsJSON r = v1 $ obj ["payment_requests" .= fmap paymentRequestDetailJSON r]---- paymentRequestDetailJSON :: PaymentRequestDetail Amount -> Object-- paymentRequestDetailJSON r = obj $ concat-- [ ["payment_request_id" .= view () r]-- , paymentRequestKV $ view _2 r-- , subscriptionKV $ view _3 r-- , billableKV $ view _4 r-- ]paymentIdJSON :: PaymentId -> ValuepaymentIdJSON = idJSON "paymentId" _PaymentId
parseEventAmendment ::ModTime ->Value ->Parser EventAmendmentparseEventAmendment t = unversion "EventAmendment" $ pwherep (Version 2 0) = parseEventAmendmentV2 tp ver = badVersion "EventAmendment" verparseEventAmendmentV2 ::ModTime ->Object ->Parser EventAmendmentparseEventAmendmentV2 t o =let parseA :: Text -> Parser EventAmendmentparseA "timeChange" = TimeChange t <$> o .: "eventTime"parseA "creditToChange" = CreditToChange t <$> parseCreditToV2 oparseA "metadataChange" = MetadataChange t <$> o .: "eventMeta"parseA tid =fail . T.unpack $ "Amendment type " <> tid <> " not recognized."in o .: "amendment" >>= parseA
p ver o = badVersion "LogEntry" ver oparseRecurrence :: Object -> Parser B.RecurrenceparseRecurrence o =let parseAnnually o' = const (pure B.Annually) <$> O.lookup "annually" o'parseMonthly o' = fmap B.Monthly . parseJSON <$> O.lookup "monthly" o'parseWeekly o' = fmap B.Weekly . parseJSON <$> O.lookup "weekly" o'parseOneTime o' = const (pure B.OneTime) <$> O.lookup "one-time" o'notFound =fail $ "Value " <> show o <> " does not represent a Recurrence value."parseV val =parseAnnually val<|> parseMonthly val<|> parseWeekly val<|> parseOneTime valin fromMaybe notFound $ parseV oparseRecurrence' :: Value -> Parser B.RecurrenceparseRecurrence' = \case(Object o) -> parseRecurrence oval -> fail $ "Value " <> show val <> " is not a JSON object."
import Data.Function( ($),(.),id,)import Data.Functor (fmap)import Data.Heap as Himport Data.List.NonEmpty as Limport Data.Map.Strict as MSimport Data.Maybe (Maybe (..))import Data.Ord( Ord (..),Ordering (..),)import Data.Ratio (Rational)import Data.Text (Text)
import qualified Data.Map.Strict as MS
workCredit :: (Foldable f) => DepF -> C.UTCTime -> f Interval -> NDTworkCredit df ptime ivals = getSum $ F.foldMap (Sum . df ptime) ivals
workCredit :: (Foldable f, HasLogEntry le) => DepF -> C.UTCTime -> f (Interval le) -> NDTworkCredit df ptime ivals = getSum $ F.foldMap (Sum . df ptime . fmap (view $ event . eventTime)) ivals
workIndex :: Foldable f => f LogEntry -> WorkIndexworkIndex logEntries =let sortedEntries = F.foldr H.insert H.empty logEntries
workIndex :: (Foldable f, HasLogEntry le, Ord o) => (le -> o) -> f le -> WorkIndex leworkIndex cmp logEntries =let sortedEntries = sortWith cmp $ toList logEntries
[Either LogEvent Interval] ->Map CreditTo (NonEmpty Interval) ->Map CreditTo (NonEmpty Interval)
[Either le (Interval le)] ->Map CreditTo (NonEmpty (Interval le)) ->Map CreditTo (NonEmpty (Interval le))
appendLogEntry :: RawIndex -> LogEntry -> RawIndexappendLogEntry idx (LogEntry k ev _) =let combine :: LogEvent -> LogEvent -> Either LogEvent Intervalcombine (StartWork t) (StopWork t') | t' > t = Right $ Interval t t'combine (e1@(StartWork _)) (e2@(StartWork _)) = Left $ max e1 e2 -- ignore redundant startscombine (e1@(StopWork _)) (e2@(StopWork _)) = Left $ min e1 e2 -- ignore redundant endscombine _ e2 = Left e2-- if the interval includes the timestamp of a start event, then allow the extension of the intervalextension :: Interval -> LogEvent -> Maybe LogEventextension ival (StartWork t)| containsInclusive t ival =Just $ StartWork (ival ^. start)extension _ _ = Nothing
appendLogEntry ::forall le.HasLogEntry le =>RawIndex le ->le ->RawIndex leappendLogEntry idx logEvent =let k = logEvent ^. logEntry . creditTo
Just (Right ival : xs) -> case extension ival ev ofJust e' -> Left e' : xsNothing -> Left ev : Right ival : xs
Just (Right ival : xs) ->case extension (view (event . eventTime) <$> ival) logEvent ofJust e' -> Left e' : xsNothing -> Left logEvent : Right ival : xs
wherecombine :: le -> le -> Either le (Interval le)combine e e' = case (e ^. event, e' ^. event) of(StartWork t, StopWork t') | t' > t -> Right $ Interval e e' -- complete interval found(StartWork t, StartWork t') -> Left $ if t > t' then e else e' -- ignore redundant starts(StopWork t, StopWork t') -> Left $ if t <= t' then e else e' -- ignore redundant ends_ -> Left e'-- if the interval includes the timestamp of a start event, then allow the extension of the intervalextension :: (Interval C.UTCTime) -> le -> Maybe leextension ival newEvent@(view event -> StartWork t)| containsInclusive t ival =Just newEvent -- replace the end of the interval with the new eventextension _ _ =Nothing
Description: (Describe migration here.)Created: 2021-01-16 05:04:54.586280477 UTCDepends: 2017-06-08_04-37-31_event-metadata-ids 2016-10-14_02-49-36_event-amendmentsApply: |CREATE TYPE amendment_t AS ENUM ('amend_event_time', 'amend_credit_to', 'amend_metadata');ALTER TABLE work_events ADD COLUMN replacement_id uuid REFERENCES work_events(id);ALTER TABLE work_events ADD COLUMN amended_by_type amendment_t;ALTER TABLE work_events ADD COLUMN amended_by_id uuid;
#!/bin/bashif [ -f ".env" ]; thensource .envfiif [ -z "${AFTOK_HOST}" ]; thenAFTOK_HOST="aftok.com"fiif [ -z "${USER}" ]; thenread -p "Username: " USERechofiread -p "Event ID: " EIDwhile [ -z "${ATYPE}" ]doread -p "Amendment Type: " ATYPEcase $ATYPE in# "CREDIT_TO")# AVALUE="creditToChange"# read -p "Raise amount, in Bitcoin satoshis: " AMOUNT# ;;"TIME")AVALUE="timeChange"read -p "Event Timestamp (yyyy-MM-ddTHH:mm:ssZ): " ATIME;;*)echo "$ATYPE is not a amendment type. Please choose \"TIME\"" # or \"CREDIT_TO\""ATYPE="";;esacdoneBODY=$(cat <<END_BODY{"schemaVersion": "2.0","amendment": "timeChange","eventTime": "$ATIME"}END_BODY)curl --verbose \${ALLOW_INSECURE} \--user $USER \--header "Content-Type: application/json" \--request PUT \--data "$BODY" \"https://$AFTOK_HOST/api/events/$EID/amend"
#!/bin/bashif [ -f ".env" ]; thensource .envfiif [ -z "${AFTOK_HOST}" ]; thenAFTOK_HOST="aftok.com"fiif [ -z "${PID}" ]; thenread -p "Project UUID: " PIDechofiif [ -z "${USER}" ]; thenread -p "Username: " USERechofiread -p "Auction Name: " NAMEread -p "Description: " DESCwhile [ -z "${CCY}" ]doread -p "Currency: " CCYcase $CCY in"BTC")CCY="satoshi"read -p "Raise amount, in Bitcoin satoshis: " AMOUNT;;"ZEC")CCY="zatoshi"read -p "Raise amount, in Zcash zatoshis: " AMOUNT;;*)echo "$CCY is not a supported currency. Please choose \"BTC\" or \"ZEC\""CCY="";;esacdoneechoread -p "Auction start date (yyyy-MM-ddThh:mm:ssZ): " STARTread -p "Auction end date (yyyy-MM-ddThh:mm:ssZ): " ENDBODY=$(cat <<END_BODY{"auction_name": "$NAME","auction_desc": "$DESC","raise_amount": {"$CCY": $AMOUNT},"auction_start": "$START","auction_end": "$END"}END_BODY)curl --verbose \${ALLOW_INSECURE} \--user $USER \--header "Content-Type: application/json" \--data "$BODY" \"https://$AFTOK_HOST/api/projects/$PID/auctions"
auctionJSON :: Auction Amount -> ValueauctionJSON x =v1 $obj[ "projectId" .= idValue (projectId . _ProjectId) x,"initiator" .= idValue (initiator . _UserId) x,"name" .= (x ^. name),"description" .= (x ^. description),"raiseAmount" .= (x ^. (raiseAmount . to amountJSON)),"auctionStart" .= (x ^. auctionStart),"auctionEnd" .= (x ^. auctionEnd)]bidIdJSON :: BidId -> ValuebidIdJSON pid = v1 $ obj ["bidId" .= (pid ^. _BidId)]
-- subscriptionJSON :: B.Subscription -> Value-- subscriptionJSON = v1 . obj . subscriptionKV---- subscriptionKV :: (KeyValue kv) => B.Subscription -> [kv]-- subscriptionKV sub =-- [ "user_id" .= idValue (B.customer . _UserId) sub,-- "billable_id" .= idValue (B.billable . B._BillableId) sub,-- "start_time" .= view B.startTime sub,-- "end_time" .= view B.endTime sub-- ]-- paymentRequestDetailsJSON :: [PaymentRequestDetail Amount] -> Value-- paymentRequestDetailsJSON r = v1 $ obj ["payment_requests" .= fmap paymentRequestDetailJSON r]---- paymentRequestDetailJSON :: PaymentRequestDetail Amount -> Object-- paymentRequestDetailJSON r = obj $ concat-- [ ["payment_request_id" .= view () r]-- , paymentRequestKV $ view _2 r-- , subscriptionKV $ view _3 r-- , billableKV $ view _4 r-- ]parseRecurrence :: Object -> Parser B.RecurrenceparseRecurrence o =let parseAnnually o' = const (pure B.Annually) <$> O.lookup "annually" o'parseMonthly o' = fmap B.Monthly . parseJSON <$> O.lookup "monthly" o'parseWeekly o' = fmap B.Weekly . parseJSON <$> O.lookup "weekly" o'parseOneTime o' = const (pure B.OneTime) <$> O.lookup "one-time" o'notFound =fail $ "Value " <> show o <> " does not represent a Recurrence value."parseV val =parseAnnually val<|> parseMonthly val<|> parseWeekly val<|> parseOneTime valin fromMaybe notFound $ parseV oparseRecurrence' :: Value -> Parser B.RecurrenceparseRecurrence' = \case(Object o) -> parseRecurrence oval -> fail $ "Value " <> show val <> " is not a JSON object."
module Aftok.Snaplet.Json( idJSON,)whereimport Aftok.Json (idValue, obj, v1)import Control.Lens (Getter)import Data.Aeson ((.=), Value)import Data.UUID (UUID)idJSON :: forall a. Text -> Getter a UUID -> a -> ValueidJSON t l a = v1 $ obj [t .= idValue l a]
projectJSON :: Project -> ValueprojectJSON p =v1 $obj[ "projectName" .= (p ^. projectName),"inceptionDate" .= (p ^. inceptionDate),"initiator" .= (p ^. initiator . _UserId)]qdbProjectJSON :: (ProjectId, Project) -> ValueqdbProjectJSON = identifiedJSON "project" (_1 . _ProjectId) (_2 . to projectJSON)
import Data.Aeson ((.=))import qualified Data.Aeson as Aimport qualified Data.Aeson.Types as A
import Data.Aeson ((.:), (.=), Value (Object), eitherDecode, object)import Data.Aeson.Types (Pair, Parser, parseEither)import qualified Data.List.NonEmpty as Limport qualified Data.Map.Strict as MS
projectWorkIndex :: S.Handler App App WorkIndex
amendEventHandler :: S.Handler App App (EventId, AmendmentId)amendEventHandler = douid <- requireUserIdeventIdBytes <- getParam "eventId"eventId <-maybe(snapError 400 "eventId parameter is required")(pure . EventId)(eventIdBytes >>= U.fromASCIIBytes)modTime <- ModTime <$> liftIO C.getCurrentTimerequestJSON <- readRequestJSON 4096either(snapError 400 . T.pack)(snapEval . amendEvent uid eventId)(parseEither (parseEventAmendment modTime) requestJSON)projectWorkIndex :: S.Handler App App (WorkIndex KeyedLogEntry)
userWorkIndex :: S.Handler App App WorkIndexuserWorkIndex = workIndex <$> userEvents
userWorkIndex :: S.Handler App App (WorkIndex KeyedLogEntry)userWorkIndex = workIndex (view logEntry) <$> userEvents
amendEventHandler :: S.Handler App App AmendmentIdamendEventHandler = douid <- requireUserIdeventIdBytes <- getParam "eventId"eventId <-maybe(snapError 400 "eventId parameter is required")(pure . EventId)(eventIdBytes >>= U.fromASCIIBytes)modTime <- ModTime <$> liftIO C.getCurrentTimerequestJSON <- readRequestJSON 4096either(snapError 400 . T.pack)(snapEval . amendEvent uid eventId)(A.parseEither (parseEventAmendment modTime) requestJSON)
------------------------ Parsing----------------------parseEventAmendment ::ModTime ->Value ->Parser EventAmendmentparseEventAmendment t = \caseObject o ->let parseA :: Text -> Parser EventAmendmentparseA "timeChange" = TimeChange t <$> o .: "eventTime"parseA "creditToChange" = CreditToChange t <$> parseCreditToV2 oparseA "metadataChange" = MetadataChange t <$> o .: "eventMeta"parseA tid =fail . T.unpack $ "Amendment type " <> tid <> " not recognized."in o .: "amendment" >>= parseAval ->fail $ "Value " <> show val <> " is not a JSON object."------------------------ Rendering----------------------logEventJSON :: LogEvent -> ValuelogEventJSON ev =object [eventName ev .= object ["eventTime" .= (ev ^. eventTime)]]logEntryFields :: LogEntry -> [Pair]logEntryFields (LogEntry c ev m) =[ "creditTo" .= creditToJSON c,"event" .= logEventJSON ev,"eventMeta" .= m]keyedLogEntryFields :: KeyedLogEntry -> [Pair]keyedLogEntryFields (KeyedLogEntry eid le) =["eventId" .= idValue _EventId eid] <> logEntryFields lekeyedLogEntryJSON :: KeyedLogEntry -> ValuekeyedLogEntryJSON kle =object (keyedLogEntryFields kle)
keyedLogEntryJSON :: (EventId, KeyedLogEntry) -> A.ValuekeyedLogEntryJSON (eid, (pid, uid, ev)) =v2
extendedLogEntryJSON :: (ProjectId, UserId, KeyedLogEntry) -> ValueextendedLogEntryJSON (pid, uid, le) =v1
<> logEntryFields ev
<> keyedLogEntryFields lepayoutsJSON :: FractionalPayouts -> ValuepayoutsJSON (Payouts m) =v1 $let payoutsRec :: (CreditTo, Rational) -> ValuepayoutsRec (c, r) =object ["creditTo" .= creditToJSON c,"payoutRatio" .= r,"payoutPercentage" .= (fromRational @Double r * 100)]in obj $ ["payouts" .= fmap payoutsRec (MS.assocs m)]workIndexJSON :: forall t. (t -> Value) -> WorkIndex t -> ValueworkIndexJSON leJSON (WorkIndex widx) =v1 $obj ["workIndex" .= fmap widxRec (MS.assocs widx)]wherewidxRec :: (CreditTo, NonEmpty (Interval t)) -> ValuewidxRec (c, l) =object[ "creditTo" .= creditToJSON c,"intervals" .= (intervalJSON leJSON <$> L.toList l)]amendEventResultJSON :: (EventId, AmendmentId) -> ValueamendEventResultJSON (eid, aid) =object[ "replacement_event" .= idValue _EventId eid,"amendment_id" .= idValue _AmendmentId aid]
serveJSON keyedLogEntryJSON $ method POST (logWorkHandler f)amendEventRoute = serveJSON amendmentIdJSON $ method PUT amendEventHandler
serveJSON extendedLogEntryJSON $ method POST (logWorkHandler f)amendEventRoute = serveJSON amendEventResultJSON $ method PUT amendEventHandler