Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
haskell-gargantext
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
195
Issues
195
List
Board
Labels
Milestones
Merge Requests
12
Merge Requests
12
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
gargantext
haskell-gargantext
Commits
339b5326
Verified
Commit
339b5326
authored
Jun 30, 2025
by
Przemyslaw Kaminski
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'dev' into dev-improve-api-version
parents
ada0b34b
eb29c06d
Pipeline
#7713
passed with stages
in 45 minutes and 36 seconds
Changes
12
Pipelines
1
Hide whitespace changes
Inline
Side-by-side
Showing
12 changed files
with
197 additions
and
68 deletions
+197
-68
Dev.hs
src/Gargantext/API/Dev.hs
+2
-2
Delete.hs
src/Gargantext/Database/Action/Delete.hs
+2
-2
Share.hs
src/Gargantext/Database/Action/Share.hs
+7
-7
User.hs
src/Gargantext/Database/Action/User.hs
+1
-1
Class.hs
src/Gargantext/Database/Class.hs
+9
-1
Prelude.hs
src/Gargantext/Database/Prelude.hs
+2
-1
Error.hs
src/Gargantext/Database/Query/Table/Node/Error.hs
+10
-4
Root.hs
src/Gargantext/Database/Query/Tree/Root.hs
+2
-2
Transactional.hs
src/Gargantext/Database/Transactional.hs
+77
-12
DocumentSearch.hs
test/Test/Database/Operations/DocumentSearch.hs
+2
-2
Transactions.hs
test/Test/Database/Transactions.hs
+64
-16
Types.hs
test/Test/Database/Types.hs
+19
-18
No files found.
src/Gargantext/API/Dev.hs
View file @
339b5326
...
@@ -55,7 +55,7 @@ defaultSettingsFile :: SettingsFile
...
@@ -55,7 +55,7 @@ defaultSettingsFile :: SettingsFile
defaultSettingsFile
=
SettingsFile
"gargantext-settings.toml"
defaultSettingsFile
=
SettingsFile
"gargantext-settings.toml"
-- | Run Cmd Sugar for the Repl (GHCI)
-- | Run Cmd Sugar for the Repl (GHCI)
runCmdRepl
::
Show
err
=>
CmdRandom
DevEnv
err
a
->
IO
a
runCmdRepl
::
(
Typeable
err
,
Show
err
)
=>
CmdRandom
DevEnv
err
a
->
IO
a
runCmdRepl
f
=
withDevEnv
defaultSettingsFile
$
\
env
->
runCmdDev
env
f
runCmdRepl
f
=
withDevEnv
defaultSettingsFile
$
\
env
->
runCmdDev
env
f
runCmdReplServantErr
::
CmdRandom
DevEnv
ServerError
a
->
IO
a
runCmdReplServantErr
::
CmdRandom
DevEnv
ServerError
a
->
IO
a
...
@@ -65,7 +65,7 @@ runCmdReplServantErr = runCmdRepl
...
@@ -65,7 +65,7 @@ runCmdReplServantErr = runCmdRepl
-- the command.
-- the command.
-- This function is constrained to the DevEnv rather than
-- This function is constrained to the DevEnv rather than
-- using HasConnectionPool and HasRepoVar.
-- using HasConnectionPool and HasRepoVar.
runCmdDev
::
Show
err
=>
DevEnv
->
CmdRandom
DevEnv
err
a
->
IO
a
runCmdDev
::
(
Typeable
err
,
Show
err
)
=>
DevEnv
->
CmdRandom
DevEnv
err
a
->
IO
a
runCmdDev
env
f
=
runCmdDev
env
f
=
either
(
fail
.
show
)
pure
=<<
runCmd
env
f
either
(
fail
.
show
)
pure
=<<
runCmd
env
f
...
...
src/Gargantext/Database/Action/Delete.hs
View file @
339b5326
...
@@ -32,7 +32,7 @@ import Gargantext.Database.GargDB qualified as GargDB
...
@@ -32,7 +32,7 @@ import Gargantext.Database.GargDB qualified as GargDB
import
Gargantext.Database.Prelude
import
Gargantext.Database.Prelude
import
Gargantext.Database.Query.Table.Node
(
getNodeWith
)
import
Gargantext.Database.Query.Table.Node
(
getNodeWith
)
import
Gargantext.Database.Query.Table.Node
qualified
as
N
(
getNode
,
deleteNode
)
import
Gargantext.Database.Query.Table.Node
qualified
as
N
(
getNode
,
deleteNode
)
import
Gargantext.Database.Query.Table.Node.Error
(
HasNodeError
,
e
rrorWith
)
import
Gargantext.Database.Query.Table.Node.Error
(
HasNodeError
,
nodeE
rrorWith
)
import
Gargantext.Database.Schema.Node
import
Gargantext.Database.Schema.Node
import
Gargantext.Prelude
import
Gargantext.Prelude
...
@@ -51,7 +51,7 @@ deleteNode u nodeId = do
...
@@ -51,7 +51,7 @@ deleteNode u nodeId = do
(
num
,
upd_node
,
cleanup
)
<-
runDBTx
$
do
(
num
,
upd_node
,
cleanup
)
<-
runDBTx
$
do
node'
<-
N
.
getNode
nodeId
node'
<-
N
.
getNode
nodeId
(
rows
,
clean_it
)
<-
case
view
node_typename
node'
of
(
rows
,
clean_it
)
<-
case
view
node_typename
node'
of
nt
|
nt
==
toDBid
NodeUser
->
e
rrorWith
"[G.D.A.D.deleteNode] Not allowed to delete NodeUser (yet)"
nt
|
nt
==
toDBid
NodeUser
->
nodeE
rrorWith
"[G.D.A.D.deleteNode] Not allowed to delete NodeUser (yet)"
nt
|
nt
==
toDBid
NodeTeam
->
do
nt
|
nt
==
toDBid
NodeTeam
->
do
uId
<-
getUserId
u
uId
<-
getUserId
u
if
_node_user_id
node'
==
uId
if
_node_user_id
node'
==
uId
...
...
src/Gargantext/Database/Action/Share.hs
View file @
339b5326
...
@@ -26,7 +26,7 @@ import Gargantext.Database.Admin.Config (hasNodeType, isInNodeTypes)
...
@@ -26,7 +26,7 @@ import Gargantext.Database.Admin.Config (hasNodeType, isInNodeTypes)
import
Gargantext.Database.Admin.Types.Hyperdata.Any
(
HyperdataAny
(
..
))
import
Gargantext.Database.Admin.Types.Hyperdata.Any
(
HyperdataAny
(
..
))
import
Gargantext.Database.Admin.Types.Node
import
Gargantext.Database.Admin.Types.Node
import
Gargantext.Database.Query.Table.Node
(
getNode
,
getNodesWith
)
import
Gargantext.Database.Query.Table.Node
(
getNode
,
getNodesWith
)
import
Gargantext.Database.Query.Table.Node.Error
(
HasNodeError
,
e
rrorWith
)
import
Gargantext.Database.Query.Table.Node.Error
(
HasNodeError
,
nodeE
rrorWith
)
import
Gargantext.Database.Query.Table.User
import
Gargantext.Database.Query.Table.User
import
Gargantext.Database.Query.Tree.Root
(
getRootId
)
import
Gargantext.Database.Query.Tree.Root
(
getRootId
)
import
Gargantext.Database.Schema.Node
import
Gargantext.Database.Schema.Node
...
@@ -98,10 +98,10 @@ shareNodeWith (ShareNodeWith_User NodeFolderShared u) n = do
...
@@ -98,10 +98,10 @@ shareNodeWith (ShareNodeWith_User NodeFolderShared u) n = do
nodeToCheck
<-
getNode
n
nodeToCheck
<-
getNode
n
userIdCheck
<-
getUserId
u
userIdCheck
<-
getUserId
u
if
not
(
hasNodeType
nodeToCheck
NodeTeam
)
if
not
(
hasNodeType
nodeToCheck
NodeTeam
)
then
e
rrorWith
"[G.D.A.S.shareNodeWith] Can share node Team only"
then
nodeE
rrorWith
"[G.D.A.S.shareNodeWith] Can share node Team only"
else
else
if
(
view
node_user_id
nodeToCheck
==
userIdCheck
)
if
(
view
node_user_id
nodeToCheck
==
userIdCheck
)
then
e
rrorWith
"[G.D.A.S.shareNodeWith] Can share to others only"
then
nodeE
rrorWith
"[G.D.A.S.shareNodeWith] Can share to others only"
else
do
else
do
folderSharedId
<-
getFolderId
u
NodeFolderShared
folderSharedId
<-
getFolderId
u
NodeFolderShared
ret
<-
shareNode
(
SourceId
folderSharedId
)
(
TargetId
n
)
ret
<-
shareNode
(
SourceId
folderSharedId
)
(
TargetId
n
)
...
@@ -111,7 +111,7 @@ shareNodeWith (ShareNodeWith_User NodeFolderShared u) n = do
...
@@ -111,7 +111,7 @@ shareNodeWith (ShareNodeWith_User NodeFolderShared u) n = do
shareNodeWith
(
ShareNodeWith_Node
NodeFolderPublic
nId
)
n
=
do
shareNodeWith
(
ShareNodeWith_Node
NodeFolderPublic
nId
)
n
=
do
nodeToCheck
<-
getNode
n
nodeToCheck
<-
getNode
n
if
not
(
isInNodeTypes
nodeToCheck
publicNodeTypes
)
if
not
(
isInNodeTypes
nodeToCheck
publicNodeTypes
)
then
e
rrorWith
$
"[G.D.A.S.shareNodeWith] Can share this nodesTypes only: "
then
nodeE
rrorWith
$
"[G.D.A.S.shareNodeWith] Can share this nodesTypes only: "
<>
(
show
publicNodeTypes
)
<>
(
show
publicNodeTypes
)
else
do
else
do
folderToCheck
<-
getNode
nId
folderToCheck
<-
getNode
nId
...
@@ -120,9 +120,9 @@ shareNodeWith (ShareNodeWith_Node NodeFolderPublic nId) n = do
...
@@ -120,9 +120,9 @@ shareNodeWith (ShareNodeWith_Node NodeFolderPublic nId) n = do
ret
<-
shareNode
(
SourceId
nId
)
(
TargetId
n
)
ret
<-
shareNode
(
SourceId
nId
)
(
TargetId
n
)
let
msgs
=
[
CE
.
UpdateTreeFirstLevel
nId
,
CE
.
UpdateTreeFirstLevel
n
]
let
msgs
=
[
CE
.
UpdateTreeFirstLevel
nId
,
CE
.
UpdateTreeFirstLevel
n
]
pure
(
ret
,
msgs
)
pure
(
ret
,
msgs
)
else
e
rrorWith
"[G.D.A.S.shareNodeWith] Can share NodeWith NodeFolderPublic only"
else
nodeE
rrorWith
"[G.D.A.S.shareNodeWith] Can share NodeWith NodeFolderPublic only"
shareNodeWith
_
_
=
e
rrorWith
"[G.D.A.S.shareNodeWith] Not implemented for this NodeType"
shareNodeWith
_
_
=
nodeE
rrorWith
"[G.D.A.S.shareNodeWith] Not implemented for this NodeType"
------------------------------------------------------------------------
------------------------------------------------------------------------
getFolderId
::
HasNodeError
err
=>
User
->
NodeType
->
DBQuery
err
x
NodeId
getFolderId
::
HasNodeError
err
=>
User
->
NodeType
->
DBQuery
err
x
NodeId
...
@@ -130,7 +130,7 @@ getFolderId u nt = do
...
@@ -130,7 +130,7 @@ getFolderId u nt = do
rootId
<-
getRootId
u
rootId
<-
getRootId
u
s
<-
getNodesWith
rootId
HyperdataAny
(
Just
nt
)
Nothing
Nothing
s
<-
getNodesWith
rootId
HyperdataAny
(
Just
nt
)
Nothing
Nothing
case
head
s
of
case
head
s
of
Nothing
->
e
rrorWith
"[G.D.A.S.getFolderId] No folder shared found"
Nothing
->
nodeE
rrorWith
"[G.D.A.S.getFolderId] No folder shared found"
Just
f
->
pure
(
_node_id
f
)
Just
f
->
pure
(
_node_id
f
)
------------------------------------------------------------------------
------------------------------------------------------------------------
...
...
src/Gargantext/Database/Action/User.hs
View file @
339b5326
...
@@ -72,7 +72,7 @@ getUsername user@(UserDBId _) = do
...
@@ -72,7 +72,7 @@ getUsername user@(UserDBId _) = do
users
<-
getUsersWithId
user
users
<-
getUsersWithId
user
case
head
users
of
case
head
users
of
Just
u
->
pure
$
userLight_username
u
Just
u
->
pure
$
userLight_username
u
Nothing
->
e
rrorWith
"G.D.A.U.getUserName: User not found with that id"
Nothing
->
nodeE
rrorWith
"G.D.A.U.getUserName: User not found with that id"
getUsername
(
RootId
rid
)
=
do
getUsername
(
RootId
rid
)
=
do
n
<-
getNode
rid
n
<-
getNode
rid
getUsername
(
UserDBId
$
_node_user_id
n
)
getUsername
(
UserDBId
$
_node_user_id
n
)
...
...
src/Gargantext/Database/Class.hs
View file @
339b5326
...
@@ -2,15 +2,16 @@
...
@@ -2,15 +2,16 @@
module
Gargantext.Database.Class
where
module
Gargantext.Database.Class
where
import
Control.Exception.Safe
(
MonadCatch
)
import
Control.Lens
(
Getter
)
import
Control.Lens
(
Getter
)
import
Control.Monad.Random
(
MonadRandom
)
import
Control.Monad.Random
(
MonadRandom
)
import
Control.Monad.Trans.Control
(
MonadBaseControl
)
import
Control.Monad.Trans.Control
(
MonadBaseControl
)
import
Data.Pool
(
Pool
)
import
Data.Pool
(
Pool
)
import
Database.PostgreSQL.Simple
(
Connection
)
import
Database.PostgreSQL.Simple
(
Connection
)
import
Gargantext.Core.Notifications.CentralExchange.Types
qualified
as
CET
import
Gargantext.Core.Config
(
HasConfig
(
..
))
import
Gargantext.Core.Config
(
HasConfig
(
..
))
import
Gargantext.Core.Mail.Types
(
HasMail
)
import
Gargantext.Core.Mail.Types
(
HasMail
)
import
Gargantext.Core.NLP
(
HasNLPServer
)
import
Gargantext.Core.NLP
(
HasNLPServer
)
import
Gargantext.Core.Notifications.CentralExchange.Types
qualified
as
CET
import
Gargantext.Prelude
import
Gargantext.Prelude
-- $typesAndConstraints
-- $typesAndConstraints
...
@@ -61,6 +62,13 @@ type IsCmd env err m =
...
@@ -61,6 +62,13 @@ type IsCmd env err m =
(
MonadReader
env
m
(
MonadReader
env
m
,
MonadError
err
m
,
MonadError
err
m
,
MonadBaseControl
IO
m
,
MonadBaseControl
IO
m
-- These 3 instances below are needed because in the transactional code
-- we can throw 'err' as an exception, which requires 'err' to be an 'Exception'
-- and thus have a 'Show' and 'Typeable' instances. The fact that we can catch
-- exceptions in the evaluator of the 'DBTx' monad code means we need a 'MonadCatch'.
,
Typeable
err
,
Show
err
,
MonadCatch
m
)
)
-- | Only the /minimum/ amount of class constraints required
-- | Only the /minimum/ amount of class constraints required
...
...
src/Gargantext/Database/Prelude.hs
View file @
339b5326
...
@@ -89,7 +89,8 @@ withConn k = do
...
@@ -89,7 +89,8 @@ withConn k = do
pool
<-
view
connPool
pool
<-
view
connPool
liftBase
$
withResource
pool
(
liftBase
.
k
)
liftBase
$
withResource
pool
(
liftBase
.
k
)
runCmd
::
env
runCmd
::
(
Show
err
,
Typeable
err
)
=>
env
->
CmdRandom
env
err
a
->
CmdRandom
env
err
a
->
IO
(
Either
err
a
)
->
IO
(
Either
err
a
)
runCmd
env
m
=
runExceptT
$
runReaderT
m
env
runCmd
env
m
=
runExceptT
$
runReaderT
m
env
...
...
src/Gargantext/Database/Query/Table/Node/Error.hs
View file @
339b5326
...
@@ -19,7 +19,7 @@ module Gargantext.Database.Query.Table.Node.Error (
...
@@ -19,7 +19,7 @@ module Gargantext.Database.Query.Table.Node.Error (
,
HasNodeError
(
..
)
,
HasNodeError
(
..
)
-- * Functions
-- * Functions
,
e
rrorWith
,
nodeE
rrorWith
,
nodeError
,
nodeError
,
nodeCreationError
,
nodeCreationError
,
nodeLookupError
,
nodeLookupError
...
@@ -28,14 +28,15 @@ module Gargantext.Database.Query.Table.Node.Error (
...
@@ -28,14 +28,15 @@ module Gargantext.Database.Query.Table.Node.Error (
)
where
)
where
import
Control.Lens
(
Prism
'
,
(
#
),
(
^?
))
import
Control.Lens
(
Prism
'
,
(
#
),
(
^?
))
import
Control.Lens
qualified
as
L
import
Data.Aeson
(
object
)
import
Data.Aeson
(
object
)
import
Data.Text
qualified
as
T
import
Data.Text
qualified
as
T
import
Gargantext.Core.Types.Individu
(
Username
)
import
Gargantext.Core.Types.Individu
(
Username
)
import
Gargantext.Database.Admin.Types.Node
(
ListId
,
NodeId
(
..
),
ContextId
,
UserId
,
ParentId
)
import
Gargantext.Database.Admin.Types.Node
(
ListId
,
NodeId
(
..
),
ContextId
,
UserId
,
ParentId
)
import
Gargantext.Database.Transactional
import
Gargantext.Prelude
hiding
(
sum
,
head
)
import
Gargantext.Prelude
hiding
(
sum
,
head
)
import
Prelude
hiding
(
null
,
id
,
map
,
sum
,
show
)
import
Prelude
hiding
(
null
,
id
,
map
,
sum
,
show
)
import
Prelude
qualified
import
Prelude
qualified
import
Gargantext.Database.Transactional
data
NodeCreationError
data
NodeCreationError
=
UserParentAlreadyExists
UserId
ParentId
=
UserParentAlreadyExists
UserId
ParentId
...
@@ -89,6 +90,9 @@ data NodeError = NoListFound ListId
...
@@ -89,6 +90,9 @@ data NodeError = NoListFound ListId
|
MoveError
NodeId
NodeId
T
.
Text
|
MoveError
NodeId
NodeId
T
.
Text
|
NodeNotExportable
NodeId
T
.
Text
|
NodeNotExportable
NodeId
T
.
Text
instance
HasNodeError
NodeError
where
_NodeError
=
L
.
prism'
Prelude
.
id
Just
instance
Prelude
.
Show
NodeError
instance
Prelude
.
Show
NodeError
where
where
show
(
NoListFound
{})
=
"No list found"
show
(
NoListFound
{})
=
"No list found"
...
@@ -106,6 +110,8 @@ instance Prelude.Show NodeError
...
@@ -106,6 +110,8 @@ instance Prelude.Show NodeError
show
(
MoveError
s
t
reason
)
=
"Moving "
<>
show
s
<>
" to "
<>
show
t
<>
" failed: "
<>
T
.
unpack
reason
show
(
MoveError
s
t
reason
)
=
"Moving "
<>
show
s
<>
" to "
<>
show
t
<>
" failed: "
<>
T
.
unpack
reason
show
(
NodeNotExportable
nid
reason
)
=
"Node "
<>
show
nid
<>
" is not exportable: "
<>
show
reason
show
(
NodeNotExportable
nid
reason
)
=
"Node "
<>
show
nid
<>
" is not exportable: "
<>
show
reason
instance
Exception
NodeError
instance
ToJSON
NodeError
where
instance
ToJSON
NodeError
where
toJSON
(
DoesNotExist
n
)
=
toJSON
(
DoesNotExist
n
)
=
object
[
(
"error"
,
"Node does not exist"
)
object
[
(
"error"
,
"Node does not exist"
)
...
@@ -135,8 +141,8 @@ instance ToJSON NodeError where
...
@@ -135,8 +141,8 @@ instance ToJSON NodeError where
class
HasNodeError
e
where
class
HasNodeError
e
where
_NodeError
::
Prism'
e
NodeError
_NodeError
::
Prism'
e
NodeError
e
rrorWith
::
HasNodeError
e
=>
Text
->
DBTx
e
r
a
nodeE
rrorWith
::
HasNodeError
e
=>
Text
->
DBTx
e
r
a
e
rrorWith
x
=
nodeError
(
NodeError
$
toException
$
userError
$
T
.
unpack
x
)
nodeE
rrorWith
x
=
nodeError
(
NodeError
$
toException
$
userError
$
T
.
unpack
x
)
nodeError
::
HasNodeError
e
=>
NodeError
->
DBTx
e
r
a
nodeError
::
HasNodeError
e
=>
NodeError
->
DBTx
e
r
a
nodeError
ne
=
dbFail
$
_NodeError
#
ne
nodeError
ne
=
dbFail
$
_NodeError
#
ne
...
...
src/Gargantext/Database/Query/Tree/Root.hs
View file @
339b5326
...
@@ -37,7 +37,7 @@ getRootId :: (HasNodeError err) => User -> DBQuery err x NodeId
...
@@ -37,7 +37,7 @@ getRootId :: (HasNodeError err) => User -> DBQuery err x NodeId
getRootId
u
=
do
getRootId
u
=
do
maybeRoot
<-
head
<$>
getRoot
u
maybeRoot
<-
head
<$>
getRoot
u
case
maybeRoot
of
case
maybeRoot
of
Nothing
->
e
rrorWith
"[G.D.Q.T.R.getRootId] No root id"
Nothing
->
nodeE
rrorWith
"[G.D.Q.T.R.getRootId] No root id"
Just
r
->
pure
(
_node_id
r
)
Just
r
->
pure
(
_node_id
r
)
getRoot
::
User
->
DBQuery
err
x
[
Node
HyperdataUser
]
getRoot
::
User
->
DBQuery
err
x
[
Node
HyperdataUser
]
...
@@ -115,7 +115,7 @@ mkCorpus :: (HasNodeError err, MkCorpus a)
...
@@ -115,7 +115,7 @@ mkCorpus :: (HasNodeError err, MkCorpus a)
mkCorpus
cName
c
rootId
userId
=
do
mkCorpus
cName
c
rootId
userId
=
do
c'
<-
mk
(
Just
cName
)
c
rootId
userId
c'
<-
mk
(
Just
cName
)
c
rootId
userId
_tId
<-
case
head
c'
of
_tId
<-
case
head
c'
of
Nothing
->
e
rrorWith
"[G.D.Q.T.Root.getOrMk...] mk Corpus failed"
Nothing
->
nodeE
rrorWith
"[G.D.Q.T.Root.getOrMk...] mk Corpus failed"
Just
c''
->
insertDefaultNode
NodeTexts
c''
userId
Just
c''
->
insertDefaultNode
NodeTexts
c''
userId
corpusId
<-
maybe
(
nodeError
NoCorpusFound
)
pure
(
head
c'
)
corpusId
<-
maybe
(
nodeError
NoCorpusFound
)
pure
(
head
c'
)
...
...
src/Gargantext/Database/Transactional.hs
View file @
339b5326
...
@@ -33,8 +33,10 @@ module Gargantext.Database.Transactional (
...
@@ -33,8 +33,10 @@ module Gargantext.Database.Transactional (
,
mkOpaInsert
,
mkOpaInsert
,
mkOpaDelete
,
mkOpaDelete
-- * Throwing errors (which allows rollbacks)
-- * Throwing
and catching
errors (which allows rollbacks)
,
dbFail
,
dbFail
,
catchDBTxError
,
handleDBTxError
)
where
)
where
import
Control.Exception.Safe
qualified
as
Safe
import
Control.Exception.Safe
qualified
as
Safe
...
@@ -42,6 +44,7 @@ import Control.Lens
...
@@ -42,6 +44,7 @@ import Control.Lens
import
Control.Monad.Base
import
Control.Monad.Base
import
Control.Monad.Error.Class
import
Control.Monad.Error.Class
import
Control.Monad.Free
import
Control.Monad.Free
import
Control.Monad.Free.Church
import
Control.Monad.Trans.Control
(
MonadBaseControl
,
control
)
import
Control.Monad.Trans.Control
(
MonadBaseControl
,
control
)
import
Data.Int
(
Int64
)
import
Data.Int
(
Int64
)
import
Data.Pool
(
withResource
,
Pool
)
import
Data.Pool
(
withResource
,
Pool
)
...
@@ -51,7 +54,12 @@ import Database.PostgreSQL.Simple.Transaction qualified as PG
...
@@ -51,7 +54,12 @@ import Database.PostgreSQL.Simple.Transaction qualified as PG
import
Gargantext.Database.Class
import
Gargantext.Database.Class
import
Opaleye
import
Opaleye
import
Prelude
import
Prelude
import
Control.Monad.Free.Church
data
DBTxException
err
=
RollbackRequested
err
deriving
(
Show
,
Eq
)
instance
(
Show
err
,
Safe
.
Typeable
err
)
=>
Safe
.
Exception
(
DBTxException
err
)
where
data
DBOperation
=
DBRead
|
DBWrite
data
DBOperation
=
DBRead
|
DBWrite
...
@@ -133,7 +141,7 @@ type DBReadOnly err r a = DBTx err DBRead a
...
@@ -133,7 +141,7 @@ type DBReadOnly err r a = DBTx err DBRead a
-- Strict constraints to perform transactional read and writes.
-- Strict constraints to perform transactional read and writes.
-- Isomorphic to a DBCmd, but it doesn't impose a 'HasConfig' constraint, as
-- Isomorphic to a DBCmd, but it doesn't impose a 'HasConfig' constraint, as
-- values can always be passed as parameters of a query or update.
-- values can always be passed as parameters of a query or update.
type
DBTxCmd
err
a
=
forall
m
env
.
(
IsCmd
env
err
m
,
HasConnectionPool
env
)
=>
m
a
type
DBTxCmd
err
a
=
forall
m
env
.
(
IsCmd
env
err
m
,
HasConnectionPool
env
,
Safe
.
MonadCatch
m
)
=>
m
a
instance
Functor
(
DBTransactionOp
err
r
)
where
instance
Functor
(
DBTransactionOp
err
r
)
where
fmap
f
=
\
case
fmap
f
=
\
case
...
@@ -179,23 +187,37 @@ withReadOnlyTransactionM conn action =
...
@@ -179,23 +187,37 @@ withReadOnlyTransactionM conn action =
-- | Run a PostgreSQL transaction, suitable for operations that mixes read and writes,
-- | Run a PostgreSQL transaction, suitable for operations that mixes read and writes,
-- and actually the only choice available to run 'DBUpdate' operations.
-- and actually the only choice available to run 'DBUpdate' operations.
runDBTx
::
DBUpdate
err
a
->
DBTxCmd
err
a
runDBTx
::
(
Show
err
,
Safe
.
Typeable
err
)
=>
DBUpdate
err
a
->
DBTxCmd
err
a
runDBTx
(
DBTx
m
)
=
do
runDBTx
(
DBTx
m
)
=
do
pool
<-
view
connPool
pool
<-
view
connPool
withResourceM
pool
$
\
conn
->
withTransactionM
conn
$
foldF
(
evalOp
conn
)
m
withResourceM
pool
$
\
conn
->
(
withTransactionM
conn
$
foldF
(
evalOp
conn
)
m
)
-- IMPORTANT: We are catching the exception (after 'withTransactionM' has run, so rollback already
-- happened) and we are rethrowing this via 'throwError', such that application code can catch this
-- via 'catchDBTxError'.
-- /NOTA BENE/: the parenthesis around 'withTransactionM' ARE NOT OPTIONAL! If we remove them, we
-- would be catching this exception from 'foldF', meaning that we wouldn't let 'withTransactionM'
-- handle it, resulting in ROLLBACK NOT HAPPENING!
`
Safe
.
catches
`
[
Safe
.
Handler
$
\
(
RollbackRequested
err
)
->
throwError
err
]
-- | Runs a DB query.
-- | Runs a DB query.
-- /NOTE/ the input type is 'DBReadOnly', i.e. a transaction where /all/
-- /NOTE/ the input type is 'DBReadOnly', i.e. a transaction where /all/
-- the operations are 'DBRead'. This makes impossible to sneak in updates
-- the operations are 'DBRead'. This makes impossible to sneak in updates
-- into otherwise read-only queries.
-- into otherwise read-only queries.
runDBQuery
::
DBReadOnly
err
r
a
->
DBTxCmd
err
a
runDBQuery
::
(
Show
err
,
Safe
.
Typeable
err
)
=>
DBReadOnly
err
r
a
->
DBTxCmd
err
a
runDBQuery
(
DBTx
m
)
=
do
runDBQuery
(
DBTx
m
)
=
do
pool
<-
view
connPool
pool
<-
view
connPool
withResourceM
pool
$
\
conn
->
withReadOnlyTransactionM
conn
$
foldF
(
evalOp
conn
)
m
withResourceM
pool
$
\
conn
->
(
withReadOnlyTransactionM
conn
$
foldF
(
evalOp
conn
)
m
)
-- IMPORTANT: Same proviso as for 'runDBTx'. Technically speaking we wouldn't need
-- to throw and catch things for a query, but we are doing so for consistency with 'runDBTx'.
`
Safe
.
catches
`
[
Safe
.
Handler
$
\
(
RollbackRequested
err
)
->
throwError
err
]
-- | The main evaluator, turns our pure operations into side-effects that run into the
-- | The main evaluator, turns our pure operations into side-effects that run into the
-- 'DBCmd'.
-- 'DBCmd'.
evalOp
::
PG
.
Connection
->
DBTransactionOp
err
r
a
->
DBTxCmd
err
a
evalOp
::
(
Show
err
,
Safe
.
Typeable
err
)
=>
PG
.
Connection
->
DBTransactionOp
err
r
a
->
DBTxCmd
err
a
evalOp
conn
=
\
case
evalOp
conn
=
\
case
PGQuery
qr
q
cc
->
cc
<$>
liftBase
(
PG
.
query
conn
qr
q
)
PGQuery
qr
q
cc
->
cc
<$>
liftBase
(
PG
.
query
conn
qr
q
)
PGUpdate
qr
a
cc
->
cc
<$>
liftBase
(
PG
.
execute
conn
qr
a
)
PGUpdate
qr
a
cc
->
cc
<$>
liftBase
(
PG
.
execute
conn
qr
a
)
...
@@ -206,7 +228,7 @@ evalOp conn = \case
...
@@ -206,7 +228,7 @@ evalOp conn = \case
OpaInsert
ins
cc
->
cc
<$>
liftBase
(
runInsert
conn
ins
)
OpaInsert
ins
cc
->
cc
<$>
liftBase
(
runInsert
conn
ins
)
OpaUpdate
upd
cc
->
cc
<$>
liftBase
(
runUpdate
conn
upd
)
OpaUpdate
upd
cc
->
cc
<$>
liftBase
(
runUpdate
conn
upd
)
OpaDelete
del
cc
->
cc
<$>
liftBase
(
runDelete
conn
del
)
OpaDelete
del
cc
->
cc
<$>
liftBase
(
runDelete
conn
del
)
DBFail
err
->
throwError
err
DBFail
err
->
liftBase
(
Safe
.
throwIO
$
RollbackRequested
err
)
evalOpaCountQuery
::
PG
.
Connection
->
Select
a
->
IO
Int
evalOpaCountQuery
::
PG
.
Connection
->
Select
a
->
IO
Int
evalOpaCountQuery
conn
sel
=
do
evalOpaCountQuery
conn
sel
=
do
...
@@ -228,6 +250,49 @@ queryOne conn q v = do
...
@@ -228,6 +250,49 @@ queryOne conn q v = do
[ ]
->
Safe
.
throwIO
$
userError
"queryOne: no result returned. Check your SQL!"
[ ]
->
Safe
.
throwIO
$
userError
"queryOne: no result returned. Check your SQL!"
_
->
Safe
.
throwIO
$
userError
"queryOne: more than one result returned. Have you used the 'RETURNING' directive?"
_
->
Safe
.
throwIO
$
userError
"queryOne: more than one result returned. Have you used the 'RETURNING' directive?"
{-
Throwing and catching exceptions in a DBTx monad
================================================
It's /VERY/ important to understand the proper way to throw and catch exceptions in a DBTx monad,
as not doing so might lead to footguns.
We need to remember that when we are composing 'DBTx' operations, we are just writing a DSL which
won't get evaluated until we call either 'runDBTx' or 'runDBQuery', therefore if some parts of
our transaction throw an error, we wouldn't know until there.
There are two types of errors we might have, and it's important to be precise in terminology:
1. IO Exception: these are being thrown by the evaluators for SQL queries, i.e. we might have
IO errors being thrown by wrongly-formatted SQL queries or the Postgres DB dying on us for any reason;
These exceptions get caught by 'withTransactionM' which allows proper rollback behavior, but crucially
these kind of exceptions gets rethrown by 'withTransactionM' and must be caught via the classic
exception handlers in upstream code, but the crucial point is that even if we don't catch them, the
transaction has been rolled back successfully;
2. Domain-specific ERRORS (not exceptions, ERRORS!) being thrown within a transaction itself via things like
'nodeError' and friends. These are errors which can be thrown because our transaction code didn't go as
planned (look for the implementation of 'insertNodeWithHyperdata' for a concrete example). These errors
are translated into the evaluator as proper exception but then caught and rethrown via 'throwError', which
is crucial, because it means that them being thrown as an exception means 'withTransactionM' can rollback
as we expect to, but upstream application code can still handle these errors via 'catchError' and friends.
In order to facilitate the handling of this, we expose the 'catchDBTxError' and 'handleDBTxError', which are
just wrappers over 'catchError' -- this is what users should be using if they want to handle domain-specific errors.
But the crucial bit, and let's state this again, is that rollbacks will happen in both scenario, which is
what we want.
-}
catchDBTxError
::
DBTxCmd
err
a
->
(
err
->
DBTxCmd
err
a
)
->
DBTxCmd
err
a
catchDBTxError
=
catchError
handleDBTxError
::
(
err
->
DBTxCmd
err
a
)
->
DBTxCmd
err
a
->
DBTxCmd
err
a
handleDBTxError
=
flip
catchError
--
--
-- Smart constructors
-- Smart constructors
--
--
...
@@ -235,9 +300,6 @@ queryOne conn q v = do
...
@@ -235,9 +300,6 @@ queryOne conn q v = do
-- we are not exposing for information hiding purposes.
-- we are not exposing for information hiding purposes.
--
--
dbFail
::
err
->
DBTx
err
r
b
dbFail
=
DBTx
.
liftF
.
DBFail
mkPGQuery
::
(
PG
.
ToRow
q
,
PG
.
FromRow
a
)
mkPGQuery
::
(
PG
.
ToRow
q
,
PG
.
FromRow
a
)
=>
PG
.
Query
=>
PG
.
Query
->
q
->
q
...
@@ -270,3 +332,6 @@ mkOpaInsert a = DBTx $ liftF (OpaInsert a id)
...
@@ -270,3 +332,6 @@ mkOpaInsert a = DBTx $ liftF (OpaInsert a id)
mkOpaDelete
::
Delete
a
->
DBUpdate
err
a
mkOpaDelete
::
Delete
a
->
DBUpdate
err
a
mkOpaDelete
a
=
DBTx
$
liftF
(
OpaDelete
a
id
)
mkOpaDelete
a
=
DBTx
$
liftF
(
OpaDelete
a
id
)
dbFail
::
err
->
DBTx
err
r
b
dbFail
=
DBTx
.
liftF
.
DBFail
test/Test/Database/Operations/DocumentSearch.hs
View file @
339b5326
...
@@ -31,7 +31,7 @@ import Gargantext.Database.Prelude
...
@@ -31,7 +31,7 @@ import Gargantext.Database.Prelude
import
Gargantext.Database.Query.Facet
import
Gargantext.Database.Query.Facet
import
Gargantext.Database.Query.Table.Node
import
Gargantext.Database.Query.Table.Node
import
Gargantext.Database.Query.Table.Node.Error
(
HasNodeError
)
import
Gargantext.Database.Query.Table.Node.Error
(
HasNodeError
)
import
Gargantext.Database.Query.Table.Node.Error
(
e
rrorWith
)
import
Gargantext.Database.Query.Table.Node.Error
(
nodeE
rrorWith
)
import
Gargantext.Database.Query.Tree.Root
import
Gargantext.Database.Query.Tree.Root
import
Gargantext.Database.Query.Table.NodeContext
(
selectCountDocs
)
import
Gargantext.Database.Query.Table.NodeContext
(
selectCountDocs
)
import
Gargantext.Database.Schema.Node
(
NodePoly
(
..
))
import
Gargantext.Database.Schema.Node
(
NodePoly
(
..
))
...
@@ -119,7 +119,7 @@ getCorporaWithParentIdOrFail parentId = do
...
@@ -119,7 +119,7 @@ getCorporaWithParentIdOrFail parentId = do
xs
<-
getCorporaWithParentId
parentId
xs
<-
getCorporaWithParentId
parentId
case
xs
of
case
xs
of
[
corpus
]
->
pure
corpus
[
corpus
]
->
pure
corpus
_
->
e
rrorWith
$
"getCorporaWithParentIdOrFail, impossible: "
<>
T
.
pack
(
show
xs
)
_
->
nodeE
rrorWith
$
"getCorporaWithParentIdOrFail, impossible: "
<>
T
.
pack
(
show
xs
)
addCorpusDocuments
::
TestEnv
->
IO
TestEnv
addCorpusDocuments
::
TestEnv
->
IO
TestEnv
addCorpusDocuments
env
=
runTestMonad
env
$
do
addCorpusDocuments
env
=
runTestMonad
env
$
do
...
...
test/Test/Database/Transactions.hs
View file @
339b5326
...
@@ -4,6 +4,7 @@
...
@@ -4,6 +4,7 @@
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeApplications #-}
{-| Tests for the transactional DB API -}
{-| Tests for the transactional DB API -}
...
@@ -16,6 +17,7 @@ import Control.Exception.Safe
...
@@ -16,6 +17,7 @@ import Control.Exception.Safe
import
Control.Exception.Safe
qualified
as
Safe
import
Control.Exception.Safe
qualified
as
Safe
import
Control.Monad.Reader
import
Control.Monad.Reader
import
Control.Monad.Trans.Control
import
Control.Monad.Trans.Control
import
Data.List.NonEmpty
qualified
as
NE
import
Data.Pool
import
Data.Pool
import
Data.Profunctor.Product.TH
(
makeAdaptorAndInstance
)
import
Data.Profunctor.Product.TH
(
makeAdaptorAndInstance
)
import
Data.String
import
Data.String
...
@@ -28,20 +30,24 @@ import Database.PostgreSQL.Simple.Options qualified as Client
...
@@ -28,20 +30,24 @@ import Database.PostgreSQL.Simple.Options qualified as Client
import
Database.PostgreSQL.Simple.SqlQQ
(
sql
)
import
Database.PostgreSQL.Simple.SqlQQ
(
sql
)
import
Database.PostgreSQL.Simple.ToField
import
Database.PostgreSQL.Simple.ToField
import
Database.Postgres.Temp
qualified
as
Tmp
import
Database.Postgres.Temp
qualified
as
Tmp
import
Gargantext.
API.Errors.Types
(
BackendInternalError
)
import
Gargantext.
Core.Types.Individu
import
Gargantext.Database.Query.Table.
Node.Error
(
errorWith
)
import
Gargantext.Database.Query.Table.
User
import
Gargantext.Database.Schema.Prelude
(
Table
(
..
))
import
Gargantext.Database.Schema.Prelude
(
Table
(
..
))
import
Gargantext.Database.Transactional
import
Gargantext.Database.Transactional
import
Gargantext.Prelude
import
Gargantext.Prelude
hiding
(
throwIO
,
catch
)
import
Opaleye
(
selectTable
,
requiredTableField
,
SqlInt4
)
import
Opaleye
(
selectTable
,
requiredTableField
,
SqlInt4
)
import
Opaleye
qualified
as
O
import
Opaleye
qualified
as
O
import
Prelude
qualified
import
Prelude
qualified
import
Shelly
as
SH
import
Shelly
as
SH
import
System.Random.Stateful
import
System.Random.Stateful
import
Test.API.Setup
(
setupEnvironment
)
import
Test.Database.Setup
import
Test.Database.Types
hiding
(
Counter
)
import
Test.Database.Types
hiding
(
Counter
)
import
Test.Hspec
import
Test.Hspec
import
Test.Tasty.HUnit
hiding
(
assert
)
import
Test.Tasty.HUnit
hiding
(
assert
)
import
Text.RawString.QQ
import
Text.RawString.QQ
import
Gargantext.Database.Action.User
import
Gargantext.Database.Query.Table.Node.Error
--
--
-- For these tests we do not want to test the normal GGTX database queries, but rather
-- For these tests we do not want to test the normal GGTX database queries, but rather
...
@@ -79,9 +85,9 @@ countersTable =
...
@@ -79,9 +85,9 @@ countersTable =
)
)
newtype
TestDBTxMonad
a
=
TestDBTxMonad
{
_TestDBTxMonad
::
TestMonadM
DBHandle
BackendInternalError
a
}
newtype
TestDBTxMonad
a
=
TestDBTxMonad
{
_TestDBTxMonad
::
TestMonadM
DBHandle
IOException
a
}
deriving
(
Functor
,
Applicative
,
Monad
deriving
(
Functor
,
Applicative
,
Monad
,
MonadReader
DBHandle
,
MonadError
BackendInternalError
,
MonadReader
DBHandle
,
MonadError
IOException
,
MonadBase
IO
,
MonadBase
IO
,
MonadBaseControl
IO
,
MonadBaseControl
IO
,
MonadFail
,
MonadFail
...
@@ -91,8 +97,12 @@ newtype TestDBTxMonad a = TestDBTxMonad { _TestDBTxMonad :: TestMonadM DBHandle
...
@@ -91,8 +97,12 @@ newtype TestDBTxMonad a = TestDBTxMonad { _TestDBTxMonad :: TestMonadM DBHandle
,
MonadThrow
,
MonadThrow
)
)
runTestDBTxMonad
::
DBHandle
->
TestMonadM
DBHandle
BackendInternalError
a
->
IO
a
runTestDBTxMonad
::
DBHandle
->
TestMonadM
DBHandle
IOException
a
->
IO
a
runTestDBTxMonad
env
=
flip
runReaderT
env
.
_TestMonad
runTestDBTxMonad
env
m
=
do
res
<-
flip
runReaderT
env
.
runExceptT
.
_TestMonad
$
m
case
res
of
Left
err
->
throwIO
$
Prelude
.
userError
(
"runTestDBTxMonad: "
<>
displayException
err
)
Right
x
->
pure
x
setup
::
IO
DBHandle
setup
::
IO
DBHandle
setup
=
do
setup
=
do
...
@@ -163,23 +173,23 @@ teardown test_db = do
...
@@ -163,23 +173,23 @@ teardown test_db = do
instance
PG
.
FromRow
Counter
where
instance
PG
.
FromRow
Counter
where
fromRow
=
Counter
<$>
field
<*>
field
fromRow
=
Counter
<$>
field
<*>
field
getCounterById
::
CounterId
->
DBQuery
BackendInternalError
r
Counter
getCounterById
::
CounterId
->
DBQuery
IOException
r
Counter
getCounterById
(
CounterId
cid
)
=
do
getCounterById
(
CounterId
cid
)
=
do
xs
<-
mkPGQuery
[
sql
|
SELECT * FROM public.ggtx_test_counter_table WHERE id = ?;
|]
(
PG
.
Only
cid
)
xs
<-
mkPGQuery
[
sql
|
SELECT * FROM public.ggtx_test_counter_table WHERE id = ?;
|]
(
PG
.
Only
cid
)
case
xs
of
case
xs
of
[
c
]
->
pure
c
[
c
]
->
pure
c
rst
->
errorWith
$
"getCounterId returned more than one result: "
<>
T
.
pack
(
show
rst
)
rst
->
dbFail
$
Prelude
.
userError
$
"getCounterId returned more than one result: "
<>
show
rst
insertCounter
::
DBUpdate
BackendInternalError
Counter
insertCounter
::
DBUpdate
IOException
Counter
insertCounter
=
do
insertCounter
=
do
mkPGUpdateReturningOne
[
sql
|
INSERT INTO public.ggtx_test_counter_table(counter_value) VALUES(0) RETURNING id, counter_value
|]
()
mkPGUpdateReturningOne
[
sql
|
INSERT INTO public.ggtx_test_counter_table(counter_value) VALUES(0) RETURNING id, counter_value
|]
()
updateCounter
::
CounterId
->
Int
->
DBUpdate
BackendInternalError
Counter
updateCounter
::
CounterId
->
Int
->
DBUpdate
IOException
Counter
updateCounter
cid
x
=
do
updateCounter
cid
x
=
do
mkPGUpdateReturningOne
[
sql
|
UPDATE public.ggtx_test_counter_table SET counter_value = ? WHERE id = ? RETURNING *
|]
(
x
,
cid
)
mkPGUpdateReturningOne
[
sql
|
UPDATE public.ggtx_test_counter_table SET counter_value = ? WHERE id = ? RETURNING *
|]
(
x
,
cid
)
-- | We deliberately write this as a composite operation.
-- | We deliberately write this as a composite operation.
stepCounter
::
CounterId
->
DBUpdate
BackendInternalError
Counter
stepCounter
::
CounterId
->
DBUpdate
IOException
Counter
stepCounter
cid
=
do
stepCounter
cid
=
do
Counter
{
..
}
<-
getCounterById
cid
Counter
{
..
}
<-
getCounterById
cid
mkPGUpdateReturningOne
[
sql
|
UPDATE public.ggtx_test_counter_table SET counter_value = ? WHERE id = ? RETURNING *
|]
(
counterValue
+
1
,
cid
)
mkPGUpdateReturningOne
[
sql
|
UPDATE public.ggtx_test_counter_table SET counter_value = ? WHERE id = ? RETURNING *
|]
(
counterValue
+
1
,
cid
)
...
@@ -189,8 +199,15 @@ stepCounter cid = do
...
@@ -189,8 +199,15 @@ stepCounter cid = do
--
--
tests
::
Spec
tests
::
Spec
tests
=
parallel
$
around
withTestCounterDB
$
tests
=
describe
"Database Transactions"
$
do
describe
"Database Transactions"
$
do
counterDBTests
ggtxDBTests
-- | Testing the transactional behaviour outside the classic GGTX operations.
-- We test that throwing exceptions in IO leads to rollbacks.
counterDBTests
::
Spec
counterDBTests
=
parallel
$
around
withTestCounterDB
$
describe
"Counter Transactions"
$
do
describe
"Opaleye count queries"
$
do
describe
"Opaleye count queries"
$
do
it
"Supports counting rows"
opaCountQueries
it
"Supports counting rows"
opaCountQueries
describe
"Pure PG Queries"
$
do
describe
"Pure PG Queries"
$
do
...
@@ -206,6 +223,14 @@ tests = parallel $ around withTestCounterDB $
...
@@ -206,6 +223,14 @@ tests = parallel $ around withTestCounterDB $
describe
"Read/Write Consistency"
$
do
describe
"Read/Write Consistency"
$
do
it
"should return a consistent state to different actors"
testConsistency
it
"should return a consistent state to different actors"
testConsistency
-- | Testing the transactional behaviour inside the classic GGTX operations.
-- We test that throwing something like a 'NodeError' results in a proper rollback.
ggtxDBTests
::
Spec
ggtxDBTests
=
parallel
$
around
withTestDB
$
beforeWith
(
\
ctx
->
setupEnvironment
ctx
>>=
(
const
$
pure
ctx
))
$
describe
"GGTX Transactions"
$
do
describe
"Rollback support"
$
do
it
"can rollback if a ggtx error gets thrown"
testGGTXErrorRollback
simplePGQueryWorks
::
DBHandle
->
Assertion
simplePGQueryWorks
::
DBHandle
->
Assertion
simplePGQueryWorks
env
=
runTestDBTxMonad
env
$
do
simplePGQueryWorks
env
=
runTestDBTxMonad
env
$
do
x
<-
runDBQuery
$
getCounterById
(
CounterId
1
)
x
<-
runDBQuery
$
getCounterById
(
CounterId
1
)
...
@@ -239,9 +264,9 @@ testRollback env = runTestDBTxMonad env $ do
...
@@ -239,9 +264,9 @@ testRollback env = runTestDBTxMonad env $ do
liftIO
$
counterValue
initialCounter
`
shouldBe
`
1
liftIO
$
counterValue
initialCounter
`
shouldBe
`
1
-- Let's do another transaction where at the very last instruction we
-- Let's do another transaction where at the very last instruction we
-- fail.
-- fail.
Safe
.
handle
(
\
(
_
::
Some
Exception
)
->
pure
()
)
$
runDBTx
$
do
handleDBTxError
(
\
(
_
::
IO
Exception
)
->
pure
()
)
$
runDBTx
$
do
_x'
<-
stepCounter
(
counterId
initialCounter
)
_x'
<-
stepCounter
(
counterId
initialCounter
)
errorWith
"urgh"
dbFail
$
Prelude
.
userError
"urgh"
-- Let's check that the second 'stepCounter' didn't actually modified the counter's value.
-- Let's check that the second 'stepCounter' didn't actually modified the counter's value.
finalCounter
<-
runDBTx
$
getCounterById
(
counterId
initialCounter
)
finalCounter
<-
runDBTx
$
getCounterById
(
counterId
initialCounter
)
...
@@ -277,3 +302,26 @@ opaCountQueries env = runTestDBTxMonad env $ do
...
@@ -277,3 +302,26 @@ opaCountQueries env = runTestDBTxMonad env $ do
_
<-
insertCounter
_
<-
insertCounter
mkOpaCountQuery
(
selectTable
countersTable
)
mkOpaCountQuery
(
selectTable
countersTable
)
liftIO
$
num
@?=
3
liftIO
$
num
@?=
3
-- | In this simple test we create a user node in GGTX, we try
-- to update it, and check that if we throw an error in the update
-- transaction, the changes are not propagated
testGGTXErrorRollback
::
TestEnv
->
Assertion
testGGTXErrorRollback
env
=
runTestMonadM
@
NodeError
env
$
do
let
ur
=
NewUser
"alfredo"
"alfredo@foo.com"
(
GargPassword
"mypass"
)
let
newUsers
=
ur
NE
.:|
[]
hashed
<-
liftIO
$
mapM
toUserHash
newUsers
void
$
runDBTx
$
insertNewUsers
hashed
-- Retrieve the user, check the details
insertedUr
<-
runDBQuery
$
getUserLightDB
(
UserName
"alfredo"
)
liftIO
$
userLight_username
insertedUr
`
shouldBe
`
"alfredo"
-- CRUCIAL bit: try to update the email, throw an exception in the same tx block
void
$
(
runDBTx
$
do
void
$
updateUserEmail
(
insertedUr
{
userLight_email
=
"alfredo@bar.com"
})
nodeError
$
NoRootFound
-- it doesn't matter which exception
)
`
catchDBTxError
`
\
(
_e
::
NodeError
)
->
pure
()
-- swallow it.
-- let's check that the email hasn't been changed.
insertedUr'
<-
runDBQuery
$
getUserLightDB
(
UserName
"alfredo"
)
liftIO
$
userLight_email
insertedUr'
`
shouldBe
`
"alfredo@foo.com"
test/Test/Database/Types.hs
View file @
339b5326
...
@@ -17,7 +17,6 @@ module Test.Database.Types where
...
@@ -17,7 +17,6 @@ module Test.Database.Types where
import
Control.Exception.Safe
import
Control.Exception.Safe
import
Control.Lens
import
Control.Lens
import
Control.Monad.Except
import
Control.Monad.Reader
import
Control.Monad.Reader
import
Control.Monad.Trans.Control
import
Control.Monad.Trans.Control
import
Data.IORef
import
Data.IORef
...
@@ -25,8 +24,7 @@ import Data.Map qualified as Map
...
@@ -25,8 +24,7 @@ import Data.Map qualified as Map
import
Data.Pool
import
Data.Pool
import
Database.PostgreSQL.Simple
qualified
as
PG
import
Database.PostgreSQL.Simple
qualified
as
PG
import
Database.Postgres.Temp
qualified
as
Tmp
import
Database.Postgres.Temp
qualified
as
Tmp
import
GHC.IO.Exception
(
userError
)
import
Gargantext
hiding
(
throwIO
,
to
)
import
Gargantext
hiding
(
to
)
import
Gargantext.API.Admin.Orchestrator.Types
import
Gargantext.API.Admin.Orchestrator.Types
import
Gargantext.API.Errors.Types
import
Gargantext.API.Errors.Types
import
Gargantext.API.Prelude
import
Gargantext.API.Prelude
...
@@ -41,6 +39,7 @@ import Gargantext.Utils.Jobs.Monad (MonadJobStatus(..))
...
@@ -41,6 +39,7 @@ import Gargantext.Utils.Jobs.Monad (MonadJobStatus(..))
import
Network.URI
(
parseURI
)
import
Network.URI
(
parseURI
)
import
Prelude
qualified
import
Prelude
qualified
import
System.Log.FastLogger
qualified
as
FL
import
System.Log.FastLogger
qualified
as
FL
import
System.IO.Error
(
userError
)
newtype
Counter
=
Counter
{
_Counter
::
IORef
Int
}
newtype
Counter
=
Counter
{
_Counter
::
IORef
Int
}
...
@@ -64,10 +63,11 @@ data TestEnv = TestEnv {
...
@@ -64,10 +63,11 @@ data TestEnv = TestEnv {
,
test_worker_tid
::
!
ThreadId
,
test_worker_tid
::
!
ThreadId
}
}
newtype
TestMonadM
env
err
a
=
TestMonad
{
_TestMonad
::
ReaderT
env
IO
a
}
newtype
TestMonadM
env
err
a
=
TestMonad
{
_TestMonad
::
ExceptT
err
(
ReaderT
env
IO
)
a
}
deriving
(
Functor
,
Applicative
,
Monad
deriving
(
Functor
,
Applicative
,
Monad
,
MonadReader
env
,
MonadReader
env
,
MonadBase
IO
,
MonadBase
IO
,
MonadError
err
,
MonadBaseControl
IO
,
MonadBaseControl
IO
,
MonadFail
,
MonadFail
,
MonadIO
,
MonadIO
...
@@ -76,10 +76,10 @@ newtype TestMonadM env err a = TestMonad { _TestMonad :: ReaderT env IO a }
...
@@ -76,10 +76,10 @@ newtype TestMonadM env err a = TestMonad { _TestMonad :: ReaderT env IO a }
,
MonadThrow
,
MonadThrow
)
)
instance
HasLogger
(
TestMonadM
TestEnv
BackendInternalErro
r
)
where
instance
HasLogger
(
TestMonadM
TestEnv
er
r
)
where
data
instance
Logger
(
TestMonadM
TestEnv
BackendInternalErro
r
)
=
TestLogger
{
_IOLogger
::
IOStdLogger
}
data
instance
Logger
(
TestMonadM
TestEnv
er
r
)
=
TestLogger
{
_IOLogger
::
IOStdLogger
}
type
instance
LogInitParams
(
TestMonadM
TestEnv
BackendInternalErro
r
)
=
LogConfig
type
instance
LogInitParams
(
TestMonadM
TestEnv
er
r
)
=
LogConfig
type
instance
LogPayload
(
TestMonadM
TestEnv
BackendInternalErro
r
)
=
Prelude
.
String
type
instance
LogPayload
(
TestMonadM
TestEnv
er
r
)
=
Prelude
.
String
initLogger
cfg
=
fmap
TestLogger
$
(
liftIO
$
ioStdLogger
cfg
)
initLogger
cfg
=
fmap
TestLogger
$
(
liftIO
$
ioStdLogger
cfg
)
destroyLogger
=
liftIO
.
_iosl_destroy
.
_IOLogger
destroyLogger
=
liftIO
.
_iosl_destroy
.
_IOLogger
logMsg
(
TestLogger
ioLogger
)
lvl
msg
=
liftIO
$
_iosl_log_msg
ioLogger
lvl
msg
logMsg
(
TestLogger
ioLogger
)
lvl
msg
=
liftIO
$
_iosl_log_msg
ioLogger
lvl
msg
...
@@ -89,18 +89,19 @@ instance MonadLogger (TestMonadM TestEnv BackendInternalError) where
...
@@ -89,18 +89,19 @@ instance MonadLogger (TestMonadM TestEnv BackendInternalError) where
getLogger
=
TestMonad
$
do
getLogger
=
TestMonad
$
do
initLogger
@
(
TestMonadM
TestEnv
BackendInternalError
)
(
LogConfig
Nothing
ERROR
)
initLogger
@
(
TestMonadM
TestEnv
BackendInternalError
)
(
LogConfig
Nothing
ERROR
)
runTestMonadM
::
env
->
TestMonadM
env
err
a
->
IO
a
runTestMonadM
::
Show
err
=>
env
->
TestMonadM
env
err
a
->
IO
a
runTestMonadM
env
=
flip
runReaderT
env
.
_TestMonad
runTestMonadM
env
m
=
do
res
<-
flip
runReaderT
env
.
runExceptT
.
_TestMonad
$
m
case
res
of
Left
err
->
throwIO
$
userError
(
show
err
)
Right
x
->
pure
x
runTestMonad
::
TestEnv
->
TestMonadM
TestEnv
BackendInternalError
a
->
IO
a
runTestMonad
::
TestEnv
->
TestMonadM
TestEnv
BackendInternalError
a
->
IO
a
runTestMonad
env
=
flip
runReaderT
env
.
_TestMonad
runTestMonad
env
m
=
do
res
<-
flip
runReaderT
env
.
runExceptT
.
_TestMonad
$
m
-- | Shoehorn a BackendInternalError into an IOException, suitable
case
res
of
-- for testing.
Left
err
->
throwIO
$
userError
(
"runTestMonad: "
<>
show
err
)
instance
MonadError
BackendInternalError
(
TestMonadM
env
BackendInternalError
)
where
Right
x
->
pure
x
throwError
e
=
TestMonad
$
throwError
(
userError
$
show
e
)
catchError
(
TestMonad
m
)
hdl
=
TestMonad
$
ReaderT
$
\
e
->
catchError
(
flip
runReaderT
e
m
)
(
\
e'
->
runTestMonadM
e
$
hdl
(
InternalWorkerError
e'
))
type
TestMonad
=
TestMonadM
TestEnv
BackendInternalError
type
TestMonad
=
TestMonadM
TestEnv
BackendInternalError
data
TestJobHandle
=
TestNoJobHandle
data
TestJobHandle
=
TestNoJobHandle
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment