diff options
| author | Ben Sima <ben@bsima.me> | 2025-11-09 07:53:04 -0500 |
|---|---|---|
| committer | Ben Sima <ben@bsima.me> | 2025-11-09 07:53:04 -0500 |
| commit | 8ae33333b0fc0ca0876681dbcd54f962b89328fe (patch) | |
| tree | c0ca7144f2082cf3ad88bca27508707642686f84 /Omni/Task/Core.hs | |
| parent | 04986e2fc5c8863672c2a84e644777505878318b (diff) | |
Protect production task database from tests and add migration
- Add TASK_TEST_MODE environment variable to use separate test database
- All file operations now use getTasksFilePath to respect test mode -
Tests use .tasks/tasks-test.jsonl instead of production database -
Add automatic migration from old task format (taskProject field)
to new format - Migrated tasks convert taskProject to WorkTask type
with empty parent - Old [Text] dependencies converted to [Dependency]
with Blocks type - Restore actual tasks from commit 3bf1691 (were
lost during testing)
This prevents accidental data loss when running tests and provides
backward compatibility for existing task databases.
Diffstat (limited to 'Omni/Task/Core.hs')
| -rw-r--r-- | Omni/Task/Core.hs | 76 |
1 files changed, 65 insertions, 11 deletions
diff --git a/Omni/Task/Core.hs b/Omni/Task/Core.hs index 1137d8d..6285ef7 100644 --- a/Omni/Task/Core.hs +++ b/Omni/Task/Core.hs @@ -5,13 +5,18 @@ module Omni.Task.Core where import Alpha +import Control.Monad ((>>=)) import Data.Aeson (FromJSON, ToJSON, decode, encode) +import qualified Data.Aeson as Aeson +import qualified Data.Aeson.KeyMap as KM +import Data.Aeson.Types (parseMaybe) import qualified Data.ByteString.Lazy.Char8 as BLC import qualified Data.Text as T import qualified Data.Text.IO as TIO import Data.Time (UTCTime, diffTimeToPicoseconds, getCurrentTime, utctDayTime) import GHC.Generics () import System.Directory (createDirectoryIfMissing, doesFileExist) +import System.Environment (lookupEnv) -- Core data types data Task = Task @@ -66,14 +71,23 @@ instance ToJSON Task instance FromJSON Task +-- Get the tasks database file path (use test file if TASK_TEST_MODE is set) +getTasksFilePath :: IO FilePath +getTasksFilePath = do + testMode <- lookupEnv "TASK_TEST_MODE" + pure <| case testMode of + Just "1" -> ".tasks/tasks-test.jsonl" + _ -> ".tasks/tasks.jsonl" + -- Initialize the task database initTaskDb :: IO () initTaskDb = do createDirectoryIfMissing True ".tasks" - exists <- doesFileExist ".tasks/tasks.jsonl" + tasksFile <- getTasksFilePath + exists <- doesFileExist tasksFile unless exists <| do - TIO.writeFile ".tasks/tasks.jsonl" "" - putText "Initialized task database at .tasks/tasks.jsonl" + TIO.writeFile tasksFile "" + putText <| "Initialized task database at " <> T.pack tasksFile -- Generate a short ID using base62 encoding of timestamp generateId :: IO Text @@ -101,13 +115,14 @@ toBase62 n = reverse <| go n [] -> '0' -- Fallback (should never happen) in char : go q --- Load all tasks from JSONL file +-- Load all tasks from JSONL file (with migration support) loadTasks :: IO [Task] loadTasks = do - exists <- doesFileExist ".tasks/tasks.jsonl" + tasksFile <- getTasksFilePath + exists <- doesFileExist tasksFile if exists then do - content <- TIO.readFile ".tasks/tasks.jsonl" + content <- TIO.readFile tasksFile let taskLines = T.lines content pure <| mapMaybe decodeTask taskLines else pure [] @@ -116,13 +131,49 @@ loadTasks = do decodeTask line = if T.null line then Nothing - else decode (BLC.pack <| T.unpack line) + else case decode (BLC.pack <| T.unpack line) of + Just task -> Just task + Nothing -> migrateOldTask line + + -- Migrate old task format (with taskProject field) to new format + migrateOldTask :: Text -> Maybe Task + migrateOldTask line = case Aeson.decode (BLC.pack <| T.unpack line) :: Maybe Aeson.Object of + Nothing -> Nothing + Just obj -> + let taskId' = KM.lookup "taskId" obj +> parseMaybe Aeson.parseJSON + taskTitle' = KM.lookup "taskTitle" obj +> parseMaybe Aeson.parseJSON + taskStatus' = KM.lookup "taskStatus" obj +> parseMaybe Aeson.parseJSON + taskCreatedAt' = KM.lookup "taskCreatedAt" obj +> parseMaybe Aeson.parseJSON + taskUpdatedAt' = KM.lookup "taskUpdatedAt" obj +> parseMaybe Aeson.parseJSON + -- Extract old taskDependencies (could be [Text] or [Dependency]) + oldDeps = KM.lookup "taskDependencies" obj +> parseMaybe Aeson.parseJSON :: Maybe [Text] + newDeps = maybe [] (map (\tid -> Dependency {depId = tid, depType = Blocks})) oldDeps + -- taskProject is ignored in new format (use epics instead) + taskType' = WorkTask -- Old tasks become WorkTask by default + taskParent' = Nothing + taskNamespace' = KM.lookup "taskNamespace" obj +> parseMaybe Aeson.parseJSON + in case (taskId', taskTitle', taskStatus', taskCreatedAt', taskUpdatedAt') of + (Just tid, Just title, Just status, Just created, Just updated) -> + Just + Task + { taskId = tid, + taskTitle = title, + taskType = taskType', + taskParent = taskParent', + taskNamespace = taskNamespace', + taskStatus = status, + taskDependencies = newDeps, + taskCreatedAt = created, + taskUpdatedAt = updated + } + _ -> Nothing -- Save a single task (append to JSONL) saveTask :: Task -> IO () saveTask task = do + tasksFile <- getTasksFilePath let json = encode task - BLC.appendFile ".tasks/tasks.jsonl" (json <> "\n") + BLC.appendFile tasksFile (json <> "\n") -- Create a new task createTask :: Text -> TaskType -> Maybe Text -> Maybe Text -> [Dependency] -> IO Task @@ -155,7 +206,8 @@ updateTaskStatus tid newStatus = do then t {taskStatus = newStatus, taskUpdatedAt = now} else t -- Rewrite the entire file (simple approach for MVP) - TIO.writeFile ".tasks/tasks.jsonl" "" + tasksFile <- getTasksFilePath + TIO.writeFile tasksFile "" traverse_ saveTask updatedTasks -- List tasks, optionally filtered by type or parent @@ -225,7 +277,8 @@ exportTasks :: IO () exportTasks = do tasks <- loadTasks -- Rewrite the entire file with deduplicated tasks - TIO.writeFile ".tasks/tasks.jsonl" "" + tasksFile <- getTasksFilePath + TIO.writeFile tasksFile "" traverse_ saveTask tasks -- Import tasks: Read from another JSONL file and merge with existing tasks @@ -252,7 +305,8 @@ importTasks filePath = do allTasks = updatedTasks ++ newTasks -- Rewrite tasks.jsonl with merged data - TIO.writeFile ".tasks/tasks.jsonl" "" + tasksFile <- getTasksFilePath + TIO.writeFile tasksFile "" traverse_ saveTask allTasks where decodeTask :: Text -> Maybe Task |
