{-# LANGUAGE QuasiQuotes #-} {-| Data type to represent tasks from the `tasks_view` -} module FullTask where import Protolude ( Applicative ((<*>)), Eq ((==)), Float, Generic, Hashable, Maybe (Just, Nothing), Show, decodeUtf8, encodeUtf8, show, toStrict, ($), (.), (<$>), (<&>), (<>), ) import Data.Aeson as Aeson (ToJSON, Value (Object)) import Data.Aeson.Text as Aeson (encodeToLazyText) import Data.Csv as Csv ( DefaultOrdered, ToField (..), ToNamedRecord, ToRecord, ) import Data.Text as T (Text, dropEnd, intercalate, split, splitOn) import Data.Yaml as Yaml (encode) import Database.SQLite.Simple ( FromRow (..), Query, ResultError (ConversionFailed), SQLData (SQLNull, SQLText), field, ) import Database.SQLite.Simple.FromField (FromField (..), fieldData, returnError) import Database.SQLite.Simple.FromRow (fieldWith) import Database.SQLite.Simple.Internal (Field (Field)) import Database.SQLite.Simple.Ok (Ok (Errors, Ok)) import Database.SQLite.Simple.QQ (sql) import GHC.Exception (errorCallException) import Prettyprinter (Pretty (pretty)) import Note (Note (..)) import Task ( Task ( awake_utc, closed_utc, due_utc, modified_utc, ready_utc, review_utc, state, ulid, waiting_utc ), TaskState, emptyTask, ) -- | Final user-facing format of tasks data FullTask = FullTask { ulid :: Text -- TODO: Use Ulid type , body :: Text , modified_utc :: Text , awake_utc :: Maybe Text , ready_utc :: Maybe Text , waiting_utc :: Maybe Text , review_utc :: Maybe Text , due_utc :: Maybe Text , closed_utc :: Maybe Text , state :: Maybe TaskState , group_ulid :: Maybe Text , repetition_duration :: Maybe Text , recurrence_duration :: Maybe Text , tags :: Maybe [Text] , notes :: Maybe [Note] , priority :: Maybe Float , user :: Text , metadata :: Maybe Aeson.Value } deriving (Generic, Show, Eq) -- For conversion from SQLite with SQLite.Simple instance FromRow FullTask where fromRow = FullTask <$> field <*> field <*> field <*> field <*> field <*> field <*> field <*> field <*> field <*> field <*> field <*> field <*> field -- Tags <*> fieldWith ( \value -> case fieldData value of SQLText txt -> -- Parse tags as a comma-separated list -- TODO: Parse tags as a JSON array Ok (Just (T.splitOn "," txt)) SQLNull -> Ok Nothing val -> Errors [ errorCallException $ "Expected a string, but got: " <> show val ] ) -- Notes <*> fieldWith ( \value -> case fieldData value of SQLText _txt -> -- TODO: Parse notes as a JSON array -- Must return a `Just` to signal that notes exist Ok (Just []) SQLNull -> Ok Nothing val -> Errors [ errorCallException $ "Expected a string, but got: " <> show val ] ) <*> field <*> field <*> ( field <&> \case Just (Object obj) -> Just $ Object obj _ -> Nothing ) instance FromField [Text] where fromField (Field (SQLText txt) _) = Ok $ split (== ',') txt fromField f = returnError ConversionFailed f "expecting SQLText column type" instance FromField [Note] where fromField (Field (SQLText txt) _) = let notes = split (== ',') txt in Ok $ notes <&> Note "" fromField f = returnError ConversionFailed f "expecting SQLText column type" instance Csv.ToField [Text] where toField = encodeUtf8 . T.intercalate "," instance Csv.ToField Value where toField = encodeUtf8 . toStrict . Aeson.encodeToLazyText -- For conversion to CSV instance ToRecord FullTask instance ToNamedRecord FullTask instance DefaultOrdered FullTask instance ToJSON FullTask instance Hashable FullTask instance Pretty FullTask where pretty = pretty . T.dropEnd 1 -- Drop trailing newline to maybe add it later . decodeUtf8 . Yaml.encode emptyFullTask :: FullTask emptyFullTask = FullTask { ulid = "" , body = "" , modified_utc = "" , awake_utc = Nothing , ready_utc = Nothing , waiting_utc = Nothing , review_utc = Nothing , due_utc = Nothing , closed_utc = Nothing , state = Nothing , group_ulid = Nothing , repetition_duration = Nothing , recurrence_duration = Nothing , tags = Nothing , notes = Nothing , priority = Nothing , user = "" , metadata = Nothing } selectQuery :: Query selectQuery = [sql| SELECT tasks_view.ulid AS ulid, body, modified_utc, awake_utc, ready_utc, waiting_utc, review_utc, due_utc, closed_utc, state, group_ulid, repetition_duration, recurrence_duration, tags, notes, priority, user, metadata |] cpTimesAndState :: FullTask -> Task cpTimesAndState (FullTask{..}) = emptyTask { Task.ulid , Task.modified_utc , Task.awake_utc , Task.ready_utc , Task.waiting_utc , Task.review_utc , Task.due_utc , Task.closed_utc , Task.state }