add schema version

This commit is contained in:
Alexander Foremny
2024-06-05 22:47:49 +02:00
parent 2064b4e776
commit bfb98d7675
11 changed files with 252 additions and 51 deletions

View File

@@ -15,6 +15,8 @@ import Data.ByteString.Lazy.UTF8 qualified as LB
import Data.ByteString.UTF8 qualified as B import Data.ByteString.UTF8 qualified as B
import Data.List import Data.List
import Data.Map qualified as M import Data.Map qualified as M
import Data.Map.Merge.Strict qualified as M
import Data.Maybe
import Data.String (IsString (fromString)) import Data.String (IsString (fromString))
import Data.Tagged (Tagged (..), untag) import Data.Tagged (Tagged (..), untag)
import Debug.Trace import Debug.Trace
@@ -25,11 +27,13 @@ import Network.HTTP.Types.Status qualified as W
import Network.Wai qualified as W import Network.Wai qualified as W
import Network.Wai.Handler.Warp qualified as W import Network.Wai.Handler.Warp qualified as W
import Options.Applicative qualified as A import Options.Applicative qualified as A
import Safe
import Store qualified as Q import Store qualified as Q
import System.Directory (setCurrentDirectory) import System.Directory (setCurrentDirectory)
import System.FilePath import System.FilePath
import System.INotify import System.INotify
import Text.Printf (printf) import Text.Printf (printf)
import Version
data Args = Args data Args = Args
{ cmd :: Cmd { cmd :: Cmd
@@ -57,7 +61,8 @@ data Repo = Repo
data Commit = Commit data Commit = Commit
{ id :: G.CommitOid GB.LgRepo, { id :: G.CommitOid GB.LgRepo,
collections :: [Collection] collections :: [Collection],
schemaVersion :: Version
} }
deriving (Show) deriving (Show)
@@ -110,22 +115,78 @@ initRepo root ref = do
Just cid <- fmap Tagged <$> G.resolveReference ref Just cid <- fmap Tagged <$> G.resolveReference ref
c <- G.lookupCommit cid c <- G.lookupCommit cid
cs <- mapM G.lookupCommit =<< G.listCommits Nothing cid cs <- mapM G.lookupCommit =<< G.listCommits Nothing cid
fmap Repo . forM cs $ \c -> do fmap (Repo . reverse) $
let cid = G.commitOid c foldM
fs <- ( \cs c -> do
fmap (filter ((== ".json") . takeExtension)) . liftIO $ let cid = G.commitOid c
Q.withStore root ref do fs <-
Q.withCommit cid Q.listAllFiles fmap (filter ((== ".json") . takeExtension)) . liftIO $
let cls = Q.withStore root ref do
M.toList . M.unionsWith (++) $ Q.withCommit cid (Q.listFiles "/")
map (\f -> M.singleton (takeDirectory f) [f]) fs let cls =
colls <- forM cls $ \(makeRelative "/" -> path, (file : files)) -> do M.toList . M.unionsWith (++) $
(value : values) <- do map (\f -> M.singleton (takeDirectory f) [f]) fs
liftIO $ Q.withStore root ref do colls <- forM cls $ \(path, (file : files)) -> do
mapM (Q.withCommit cid . Q.readFile) (file : files) (value : values) <- do
let schema = fromAutoTypes path $ U.autoTypes' value values liftIO $ Q.withStore root ref do
pure $ Collection path files schema mapM (Q.withCommit cid . Q.readFile) (file : files)
pure (Commit cid colls) let schema = fromAutoTypes path $ U.autoTypes' value values
pure $ Collection path files schema
let schemaVersion =
case lastMay cs of
Nothing -> Version 1 0 0
Just c' ->
let Version major' minor' patch' = c'.schemaVersion
schemas' =
M.fromList
( (\coll -> (coll.path, coll.schema))
<$> c'.collections
)
schemas =
M.fromList
( (\coll -> (coll.path, coll.schema))
<$> c.collections
)
in case compareSchemas schemas' schemas of
Just Major -> Version (major' + 1) 0 0
Just Minor -> Version major' (minor' + 1) 0
Just Patch -> Version major' minor' (patch' + 1)
Nothing -> Version major' minor' patch'
c = Commit cid colls schemaVersion
pure (c : cs)
)
[]
cs
compareSchemas ::
M.Map String Schema ->
M.Map String Schema ->
Maybe SchemaDifference
compareSchemas schemas' schemas =
maximumMay
. catMaybes
. M.elems
. M.map (uncurry compareSchemas')
$ M.merge
(M.mapMissing (\_ schema' -> (Just schema', Nothing)))
(M.mapMissing (\_ schema -> (Nothing, Just schema)))
(M.zipWithMatched (\_ schema' schema -> (Just schema', Just schema)))
schemas'
schemas
where
compareSchemas' Nothing (Just _) = Just Patch
compareSchemas' (Just _) Nothing = Just Patch
compareSchemas' (Just schema') (Just schema) = compareSchema schema' schema
-- TODO
compareSchema :: Schema -> Schema -> Maybe SchemaDifference
compareSchema schema' schema = Nothing
data SchemaDifference
= Major
| Minor
| Patch
deriving (Eq, Ord)
main :: IO () main :: IO ()
main = do main = do
@@ -146,21 +207,28 @@ main = do
q <- q <-
fromString @Q.Query . LB.toString fromString @Q.Query . LB.toString
<$> W.lazyRequestBody req <$> W.lazyRequestBody req
r <- liftIO $ Q.withStore root ref (Q.query q) r <- liftIO $ Q.withStore root ref do Q.query q
respond . W.responseLBS W.status200 [] $ J.encode r respond . W.responseLBS W.status200 [] $ J.encode r
(Debug.Trace.traceShowId -> !_) -> Right SchemaVersion -> do
repo <- atomically (readTMVar repoT)
respond $
W.responseLBS W.status200 [] $
J.encode (last repo.commits).schemaVersion
(traceShowId -> !_) ->
respond $ W.responseLBS W.status200 [] "not implemented" respond $ W.responseLBS W.status200 [] "not implemented"
data Route data Route
= SchemaJson String = SchemaJson String
| Query | Query
| SchemaVersion
deriving (Show) deriving (Show)
routeP :: P.Parser Route routeP :: P.Parser Route
routeP = routeP =
( P.choice ( P.choice
[ SchemaJson <$> (P.string "/" *> P.manyTill P.anyChar (P.string ".schema.json")), [ SchemaJson <$> (P.string "/" *> P.manyTill P.anyChar (P.string ".schema.json")),
pure Query <* (P.string "/") pure SchemaVersion <* P.string "/schemaVersion",
pure Query <* P.string "/"
] ]
) )
<* P.endOfInput <* P.endOfInput

View File

@@ -23,6 +23,7 @@ executable backend
autotypes, autotypes,
base, base,
bytestring, bytestring,
common,
containers, containers,
directory, directory,
filepath, filepath,
@@ -33,8 +34,11 @@ executable backend
http-types, http-types,
mtl, mtl,
optparse-applicative, optparse-applicative,
safe,
split,
stm, stm,
tagged, tagged,
text,
utf8-string, utf8-string,
wai, wai,
warp warp

5
common/CHANGELOG.md Normal file
View File

@@ -0,0 +1,5 @@
# Revision history for common
## 0.1.0.0 -- YYYY-mm-dd
* First version. Released on an unsuspecting world.

30
common/LICENSE Normal file
View File

@@ -0,0 +1,30 @@
Copyright (c) 2024, Alexander Foremny
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
* Neither the name of Alexander Foremny nor the names of other
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

21
common/common.cabal Normal file
View File

@@ -0,0 +1,21 @@
cabal-version: 3.4
name: common
version: 0.1.0.0
license: BSD-3-Clause
license-file: LICENSE
maintainer: aforemny@posteo.de
author: Alexander Foremny
build-type: Simple
extra-doc-files: CHANGELOG.md
library
exposed-modules: Version
hs-source-dirs: src
default-language: GHC2021
default-extensions: ViewPatterns
ghc-options: -Wall
build-depends:
aeson,
base,
split,
text

36
common/src/Version.hs Normal file
View File

@@ -0,0 +1,36 @@
module Version
( Version (..),
versionToString,
versionFromText,
versionFromString,
)
where
import Data.Aeson qualified as A
import Data.Aeson.Types qualified as A
import Data.List
import Data.List.Split
import Data.Text qualified as T
data Version = Version Int Int Int
deriving (Show, Eq)
instance A.ToJSON Version where
toJSON =
A.toJSON . versionToString
instance A.FromJSON Version where
parseJSON (A.String (versionFromText -> Just version)) = pure version
parseJSON v = A.typeMismatch "version" v
versionToString :: Version -> String
versionToString (Version major minor patch) =
intercalate "." (map show [major, minor, patch])
versionFromString :: String -> Maybe Version
versionFromString (map read . splitOn "." -> [major, minor, patch]) =
Just (Version major minor patch)
versionFromString _ = Nothing
versionFromText :: T.Text -> Maybe Version
versionFromText = versionFromString . T.unpack

View File

@@ -7,6 +7,7 @@ let
astore = self.callCabal2nix "astore" sources.json2sql { }; astore = self.callCabal2nix "astore" sources.json2sql { };
autotypes = self.callCabal2nix "autotypes" ./autotypes { }; autotypes = self.callCabal2nix "autotypes" ./autotypes { };
backend = self.callCabal2nix "backend" ./backend { }; backend = self.callCabal2nix "backend" ./backend { };
common = self.callCabal2nix "common" ./common { };
frontend = self.callCabal2nix "frontend" ./frontend { }; frontend = self.callCabal2nix "frontend" ./frontend { };
websockets = pkgs.haskell.lib.doJailbreak super.websockets; websockets = pkgs.haskell.lib.doJailbreak super.websockets;
}; };
@@ -14,6 +15,7 @@ let
jsHaskellPackages = pkgs.pkgsCross.ghcjs.haskell.packages.ghc98.override { jsHaskellPackages = pkgs.pkgsCross.ghcjs.haskell.packages.ghc98.override {
overrides = self: super: { overrides = self: super: {
common = self.callCabal2nix "common" ./common { };
frontend = self.callCabal2nix "frontend" ./frontend { }; frontend = self.callCabal2nix "frontend" ./frontend { };
}; };
}; };
@@ -25,6 +27,7 @@ rec {
packages = _: [ packages = _: [
haskellPackages.autotypes haskellPackages.autotypes
haskellPackages.backend haskellPackages.backend
haskellPackages.common
haskellPackages.frontend haskellPackages.frontend
]; ];
buildInputs = [ buildInputs = [

View File

@@ -2,6 +2,7 @@
module Api module Api
( fetchSchema, ( fetchSchema,
fetchSchemaVersion,
fetchPosts, fetchPosts,
fetchPost, fetchPost,
updatePost, updatePost,
@@ -25,11 +26,16 @@ import Data.Function
import Miso import Miso
import Safe import Safe
import Schema import Schema
import Version
fetchSchema :: JSM (Either String Schema) fetchSchema :: JSM (Either String Schema)
fetchSchema = fetchSchema =
A.eitherDecode <$> fetch (fromString "http://localhost:8081/posts.schema.json") A.eitherDecode <$> fetch (fromString "http://localhost:8081/posts.schema.json")
fetchSchemaVersion :: JSM (Either String Version)
fetchSchemaVersion =
A.eitherDecode <$> fetch (fromString "http://localhost:8081/schemaVersion")
fetchPosts :: JSM (Either String [A.Value]) fetchPosts :: JSM (Either String [A.Value])
fetchPosts = fetchPosts =
A.eitherDecode A.eitherDecode

View File

@@ -4,24 +4,38 @@ module Main where
import Language.Javascript.JSaddle.Warp as JSaddle import Language.Javascript.JSaddle.Warp as JSaddle
#endif #endif
import Api
import Data.Bifunctor import Data.Bifunctor
import Data.Default import Data.Default
import Data.Function import Data.Function
import GHC.Generics (Generic)
import Miso import Miso
import Miso.String (toMisoString) import Miso.String (toMisoString)
import NeatInterpolation qualified as Q import NeatInterpolation qualified as Q
import Page (Page, initialPage, updatePage, viewPage) import Page (Page, initialPage, updatePage, viewPage)
import Page qualified as Page import Page qualified as Page
import Route (parseURI) import Route (parseURI)
import Version
data Model = Model data Model
{ page :: Maybe (Either String Page) = Loading
| Failed String
| Loaded LoadedState
deriving (Show, Eq)
data LoadedState = LoadedState
{ page :: Maybe (Either String Page),
schemaVersion :: Version
} }
deriving (Show, Eq, Generic, Default) deriving (Show, Eq)
instance Default Model where
def = Loading
data Action data Action
= NoOp = -- Loading
SetLoaded (Either String LoadedState)
| -- Loaded
NoOp
| Init URI | Init URI
| HandleURI URI | HandleURI URI
| HandlePage Page.Action | HandlePage Page.Action
@@ -50,34 +64,43 @@ main = runApp $ do
logLevel = Off logLevel = Off
updateModel :: Action -> Model -> Effect Action Model updateModel :: Action -> Model -> Effect Action Model
updateModel NoOp m = noEff m updateModel _ (Failed err) = noEff (Failed err)
updateModel (Init uri) m = updateModel (Init uri) Loading =
m <# do Loading <# do
SetPage <$> initialPage (parseURI uri) page <- Just <$> initialPage (parseURI uri)
updateModel (HandleURI uri) m = schemaVersion' <- fetchSchemaVersion
m <# do pure $ SetLoaded do
schemaVersion <- schemaVersion'
pure LoadedState {..}
updateModel (Init _) m = noEff m
updateModel (SetLoaded (Left err)) Loading = noEff (Failed err)
updateModel (SetLoaded (Right state)) Loading = noEff (Loaded state)
updateModel (SetLoaded _) m = noEff m
updateModel _ Loading = noEff Loading
updateModel NoOp (Loaded s) = noEff (Loaded s)
updateModel (HandleURI uri) (Loaded s) =
Loaded s <# do
let route = parseURI uri let route = parseURI uri
SetPage <$> initialPage route SetPage <$> initialPage route
updateModel (SetPage page) m = noEff m {page = Just page} updateModel (SetPage page) (Loaded s) = noEff (Loaded s {page = Just page})
updateModel (HandlePage action) m = updateModel (HandlePage action) (Loaded s) =
case m.page of case s.page of
Just (Right page) -> Just (Right page) ->
updatePage action page fmap Loaded $
& bimap HandlePage (\page -> m {page = Just (Right page)}) updatePage action page
_ -> noEff m & bimap HandlePage (\page -> s {page = Just (Right page)})
_ -> noEff (Loaded s)
viewModel :: Model -> View Action viewModel :: Model -> View Action
viewModel model = viewModel Loading = text ".."
viewModel (Failed s) = err s
viewModel (Loaded s) =
div_ [] $ div_ [] $
[ viewCss, [ viewCss,
viewHeader, viewHeader s,
nav_ [] [viewCollections], nav_ [] [viewCollections],
main_ [] $ main_ [] $
[ HandlePage [ HandlePage <$> maybe (text "..") (either err viewPage) s.page
<$> maybe
(text "..")
(either err viewPage)
model.page
] ]
] ]
@@ -171,16 +194,19 @@ th, td {
err :: String -> View action err :: String -> View action
err = text . toMisoString . ("err! " <>) err = text . toMisoString . ("err! " <>)
viewHeader :: View Action viewHeader :: LoadedState -> View Action
viewHeader = viewHeader s =
header_ [] $ header_ [] $
[ section_ [] [h1_ [] [a_ [href_ "#"] [text "acms"]]], [ section_ [] [h1_ [] [a_ [href_ "#"] [text "acms"]]],
section_ [] [viewBranch] section_ [] (viewBranch s)
] ]
viewBranch :: View Action viewBranch :: LoadedState -> [View Action]
viewBranch = viewBranch s =
select_ [] [option_ [] [text "main"]] [ text (toMisoString (versionToString s.schemaVersion)),
text " ",
select_ [] [option_ [] [text "main"]]
]
viewCollections :: View Action viewCollections :: View Action
viewCollections = viewCollections =

View File

@@ -37,11 +37,13 @@ executable frontend
attoparsec, attoparsec,
base, base,
bytestring, bytestring,
common,
containers, containers,
data-default, data-default,
miso, miso,
neat-interpolation, neat-interpolation,
safe, safe,
split,
text, text,
utf8-string utf8-string

View File

@@ -2,7 +2,7 @@
"json2sql": { "json2sql": {
"branch": "main", "branch": "main",
"repo": "git@code.nomath.org:~/json2sql", "repo": "git@code.nomath.org:~/json2sql",
"rev": "d8b2af98f594e4fc5cc4919c1efe95e1d8d9aafe", "rev": "906d9ebba1ae08ea73acb55b536ff2f49e1b55c0",
"type": "git" "type": "git"
}, },
"nixpkgs": { "nixpkgs": {