1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
{-| Module : Gargantext.Database.Select.Table.NodeNode
Description :
Copyright : (c) CNRS, 2017-Present
License : AGPL + CECILL v3
Maintainer : team@gargantext.org
Stability : experimental
Portability : POSIX
Here is a longer description of this module, containing some
commentary with @some markup@.
-}
{-# LANGUAGE Arrows #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
module Gargantext.Database.Query.Table.NodeNode
( module Gargantext.Database.Schema.NodeNode
-- * Types
, SourceId(..)
, TargetId(..)
, OwnerId(..)
, PublishedNodeInfo(..)
-- * Queries
, getNodeNode
, getNodeNode2
, isNodeReadOnly
, selectDocNodes
, selectDocs
, selectDocsDates
, selectPublicNodes
, selectPublishedNodes
-- * Destructive operations
, deleteNodeNode
, nodeNodesCategory
, nodeNodesScore
, pairCorpusWithAnnuaire
, publishNode
, unpublishNode
, queryNodeNodeTable
, shareNode
-- * Internals (use with caution)
, insertNodeNode
)
where
import Control.Arrow (returnA)
import Control.Lens (view)
import Database.PostgreSQL.Simple qualified as PGS
import Database.PostgreSQL.Simple.SqlQQ (sql)
import Database.PostgreSQL.Simple.Types (Values(..), QualifiedIdentifier(..), Only (..))
import Data.Text (splitOn)
import Gargantext.Core ( HasDBid(toDBid) )
import Gargantext.Database.Admin.Types.Hyperdata.Document ( HyperdataDocument, hd_publication_date )
import Gargantext.Database.Admin.Types.Hyperdata.Prelude ( Hyperdata )
import Gargantext.Database.Admin.Types.Node
import Gargantext.Database.Prelude
import Gargantext.Database.Query.Table.Node.Error (HasNodeError)
import Gargantext.Database.Query.Table.Node (getNode)
import Gargantext.Database.Schema.Ngrams ()
import Gargantext.Database.Schema.Node
import Gargantext.Database.Schema.NodeNode
import Gargantext.Prelude
import Opaleye
import Opaleye qualified as O
import qualified Control.Lens as L
queryNodeNodeTable :: Select NodeNodeRead
queryNodeNodeTable = selectTable nodeNodeTable
-- | not optimized (get all ngrams without filters)
_nodesNodes :: DBQuery err x [NodeNode]
_nodesNodes = mkOpaQuery queryNodeNodeTable
------------------------------------------------------------------------
-- | Basic NodeNode tools
getNodeNode :: NodeId -> DBQuery err x [NodeNode]
getNodeNode n = mkOpaQuery (selectNodeNode $ pgNodeId n)
where
selectNodeNode :: Column SqlInt4 -> Select NodeNodeRead
selectNodeNode n' = proc () -> do
ns <- queryNodeNodeTable -< ()
restrict -< _nn_node1_id ns .== n'
returnA -< ns
getNodeNode2 :: NodeId -> DBQuery err x (Maybe NodeNode)
getNodeNode2 n = listToMaybe <$> mkOpaQuery (selectNodeNode $ pgNodeId n)
where
selectNodeNode :: Column SqlInt4 -> Select NodeNodeRead
selectNodeNode n' = proc () -> do
ns <- queryNodeNodeTable -< ()
restrict -< _nn_node2_id ns .== n'
returnA -< ns
------------------------------------------------------------------------
-- TODO (refactor with Children)
{-
getNodeNodeWith :: NodeId -> proxy a -> Maybe NodeType -> DBCmd err [a]
getNodeNodeWith pId _ maybeNodeType = mkOpaQuery query
where
query = selectChildren pId maybeNodeType
selectChildren :: ParentId
-> Maybe NodeType
-> Select NodeRead
selectChildren parentId maybeNodeType = proc () -> do
row@(Node nId typeName _ parent_id _ _ _) <- queryNodeTable -< ()
(NodeNode _ n1id n2id _ _) <- queryNodeNodeTable -< ()
let nodeType = maybe 0 toDBid maybeNodeType
restrict -< typeName .== sqlInt4 nodeType
restrict -< (.||) (parent_id .== (pgNodeId parentId))
( (.&&) (n1id .== pgNodeId parentId)
(n2id .== nId))
returnA -< row
-}
------------------------------------------------------------------------
-- | Inserts a list of 'NodeNode', creating relationship between nodes
-- in the database. This function is deliberately not exposed, because
-- it's low-level and it doesn't do any business-logic check to ensure
-- the share being created is valid. Use the other functions like
-- 'shareNode', 'publishNode', or roll your own.
insertNodeNode :: [NodeNode] -> DBUpdate err Int
insertNodeNode ns = fromIntegral <$> (mkOpaInsert $ Insert nodeNodeTable ns' rCount (Just doNothing))
where
ns' :: [NodeNodeWrite]
ns' = map (\(NodeNode n1 n2 x y)
-> NodeNode (pgNodeId n1)
(pgNodeId n2)
(sqlDouble <$> x)
(sqlInt4 . toDBid <$> y)
) ns
------------------------------------------------------------------------
type Node1_Id = NodeId
type Node2_Id = NodeId
deleteNodeNode :: Node1_Id -> Node2_Id -> DBUpdate err Int
deleteNodeNode n1 n2 =
fromIntegral <$> mkOpaDelete
(Delete nodeNodeTable
(\(NodeNode n1_id n2_id _ _) -> n1_id .== pgNodeId n1
.&& n2_id .== pgNodeId n2
)
rCount
)
------------------------------------------------------------------------
-- | Favorite management
_nodeNodeCategory :: CorpusId -> DocId -> Int -> DBUpdate err [Int]
_nodeNodeCategory cId dId c = map (\(PGS.Only a) -> a) <$> mkPGUpdateReturningMany favQuery (c,cId,dId)
where
favQuery :: PGS.Query
favQuery = [sql|UPDATE nodes_nodes SET category = ?
WHERE node1_id = ? AND node2_id = ?
RETURNING node2_id;
|]
nodeNodesCategory :: [(CorpusId, DocId, Int)] -> DBQuery err x [Int]
nodeNodesCategory inputData = map (\(PGS.Only a) -> a)
<$> mkPGQuery catQuery (PGS.Only $ Values fields inputData)
where
fields = map (QualifiedIdentifier Nothing) ["int4","int4","int4"]
catQuery :: PGS.Query
catQuery = [sql| UPDATE nodes_nodes as nn0
SET category = nn1.category
FROM (?) as nn1(node1_id,node2_id,category)
WHERE nn0.node1_id = nn1.node1_id
AND nn0.node2_id = nn1.node2_id
RETURNING nn1.node2_id
|]
------------------------------------------------------------------------
-- | Score management
_nodeNodeScore :: CorpusId -> DocId -> Int -> DBQuery err x [Int]
_nodeNodeScore cId dId c = map (\(PGS.Only a) -> a) <$> mkPGQuery scoreQuery (c,cId,dId)
where
scoreQuery :: PGS.Query
scoreQuery = [sql|UPDATE nodes_nodes SET score = ?
WHERE node1_id = ? AND node2_id = ?
RETURNING node2_id;
|]
nodeNodesScore :: [(CorpusId, DocId, Int)] -> DBQuery err x [Int]
nodeNodesScore inputData = map (\(PGS.Only a) -> a)
<$> mkPGQuery catScore (PGS.Only $ Values fields inputData)
where
fields = map (QualifiedIdentifier Nothing) ["int4","int4","int4"]
catScore :: PGS.Query
catScore = [sql| UPDATE nodes_nodes as nn0
SET score = nn1.score
FROM (?) as nn1(node1_id, node2_id, score)
WHERE nn0.node1_id = nn1.node1_id
AND nn0.node2_id = nn1.node2_id
RETURNING nn1.node2_id
|]
------------------------------------------------------------------------
_selectCountDocs :: HasDBid NodeType => CorpusId -> DBQuery err x Int
_selectCountDocs cId = mkOpaCountQuery (queryCountDocs cId)
where
queryCountDocs cId' = proc () -> do
(n, nn) <- joinInCorpus -< ()
restrict -< matchMaybe nn $ \case
Nothing -> toFields True
Just nn' -> (nn' ^. nn_node1_id) .== pgNodeId cId' .&&
(nn' ^. nn_category) .>= sqlInt4 1
restrict -< n^.node_typename .== sqlInt4 (toDBid NodeDocument)
returnA -< n
-- | TODO use UTCTime fast
selectDocsDates :: HasDBid NodeType => CorpusId -> DBQuery err x [Text]
selectDocsDates cId = map (head' "selectDocsDates" . splitOn "-")
<$> catMaybes
<$> map (view hd_publication_date)
<$> selectDocs cId
selectDocs :: HasDBid NodeType => CorpusId -> DBQuery err x [HyperdataDocument]
selectDocs cId = mkOpaQuery (queryDocs cId)
queryDocs :: HasDBid NodeType => CorpusId -> O.Select (Column SqlJsonb)
queryDocs cId = proc () -> do
(n, nn) <- joinInCorpus -< ()
restrict -< matchMaybe nn $ \case
Nothing -> toFields True
Just nn' -> (nn' ^. nn_node1_id) .== pgNodeId cId .&&
(nn' ^. nn_category) .>= sqlInt4 1
restrict -< n ^. node_typename .== (sqlInt4 $ toDBid NodeDocument)
returnA -< view node_hyperdata n
selectDocNodes :: HasDBid NodeType => CorpusId -> DBQuery err x [Node HyperdataDocument]
selectDocNodes cId = mkOpaQuery (queryDocNodes cId)
queryDocNodes :: HasDBid NodeType =>CorpusId -> O.Select NodeRead
queryDocNodes cId = proc () -> do
(n, nn) <- joinInCorpus -< ()
restrict -< matchMaybe nn $ \case
Nothing -> toFields True
Just nn' -> (nn' ^.nn_node1_id .== pgNodeId cId) .&&
(nn' ^. nn_category) .>= sqlInt4 1
restrict -< n^.node_typename .== sqlInt4 (toDBid NodeDocument)
returnA -< n
joinInCorpus :: O.Select (NodeRead, MaybeFields NodeNodeRead)
joinInCorpus = proc () -> do
n <- queryNodeTable -< ()
nn <- optionalRestrict queryNodeNodeTable -<
(\nn' -> (nn' ^. nn_node2_id) .== view node_id n)
returnA -< (n, nn)
------------------------------------------------------------------------
-- | Returns /all/ the public nodes, i.e. nodes which 'NodeType' is
-- 'NodeFolderPublic'. Each user, upon creation, receives his/her personal
-- public folder. Nodes placed inside /any/ public folder is visible into
-- /any other/ public folder.
selectPublicNodes :: HasDBid NodeType => (Hyperdata a, DefaultFromField SqlJsonb a)
=> DBQuery err x [(Node a, Maybe Int)]
selectPublicNodes = mkOpaQuery (queryWithType NodeFolderPublic)
data PublishedNodeInfo
= PublishedNodeInfo
{ pni_source_id :: !SourceId
, pni_target_id :: !TargetId
, pni_owner_id :: !OwnerId
, pni_policy :: !NodePublishPolicy
} deriving (Show, Eq)
selectPublishedNodes :: DBQuery err x [PublishedNodeInfo]
selectPublishedNodes =
mapMaybe mk_info <$> published_node_ids []
where
mk_info :: (NodeId, NodeNode) -> Maybe PublishedNodeInfo
mk_info (owner, nn) =
PublishedNodeInfo <$> (pure $ SourceId $ _nn_node2_id nn)
<*> (pure $ TargetId $ _nn_node1_id nn)
<*> (pure $ OwnerId owner)
<*> (nn L.^? (nn_category . L._Just . _NNC_publish))
published_node_ids :: [ NodeNodeRead -> Field SqlBool ] -> DBQuery err x [(NodeId, NodeNode)]
published_node_ids extraPreds = mkOpaQuery $ do
n <- queryNodeTable
nn <- queryNodeNodeTable
let isRO = ors [ (nn ^. nn_category .== sqlInt4 (toDBid $ NNC_publish ro))
| ro <- [minBound .. maxBound]
]
where_ isRO
where_ $ (n ^. node_id .== nn ^. nn_node1_id)
where_ $ ands (map ($ nn) extraPreds)
pure (n ^. node_parent_id, nn)
where
ands :: Foldable f => f (Field SqlBool) -> Field SqlBool
ands = foldl' (.&&) (sqlBool True)
-- | A 'Node' is read-only if there exist a match in the node_nodes directory
-- where the source is a public folder. Certain category of nodes (like private/shared folders, etc)
-- are automatically read-only.
isNodeReadOnly :: (HasNodeError err, HasDBid NodeType) => NodeId -> DBQuery err x Bool
isNodeReadOnly targetNodeId = do
targetNode <- getNode targetNodeId
case targetNode ^. node_typename `elem` map toDBid typesWhiteList of
True -> pure True
False -> is_read_only_query
where
-- Certain kind of nodes are by default read-only and can in principle be visualised by other users
-- without harm. This would be the case for a user node which might contained published corpuses.
typesWhiteList :: [ NodeType ]
typesWhiteList = [ NodeFolderPublic ]
is_read_only_query = (== [Only True])
<$> mkPGQuery [sql|
WITH RECURSIVE ParentNodes AS (
-- Base case: Start from the given node ID
SELECT id, parent_id
FROM nodes
WHERE id = ?
UNION ALL
-- Recursive case: Traverse to parent nodes
SELECT n.id, n.parent_id
FROM nodes n
JOIN ParentNodes pn ON n.id = pn.parent_id
)
SELECT EXISTS (
SELECT 1
FROM ParentNodes pn
JOIN nodes_nodes nn ON pn.id = nn.node1_id OR pn.id = nn.node2_id
JOIN nodes n ON (nn.node1_id = n.id OR nn.node2_id = n.id)
WHERE n.typename = ? AND nn.category <= ?
) OR EXISTS (
SELECT 1
FROM nodes
WHERE id = ? AND typename = ? -- if the target is a public folder, it's automatically considered read-only
) AS is_read_only;
|] ( targetNodeId
, toDBid NodeFolderPublic
, toDBid (maxBound @NodePublishPolicy)
, targetNodeId
, toDBid NodeFolderPublic
)
queryWithType :: HasDBid NodeType
=> NodeType
-> O.Select (NodeRead, MaybeFields (Column SqlInt4))
queryWithType nt = proc () -> do
(n, nn_node2_id') <- node_NodeNode -< ()
restrict -< n^.node_typename .== sqlInt4 (toDBid nt)
returnA -< (n, nn_node2_id')
node_NodeNode :: O.Select (NodeRead, MaybeFields (Field SqlInt4))
node_NodeNode = proc () -> do
n <- queryNodeTable -< ()
nn <- optionalRestrict queryNodeNodeTable -<
(\nn' -> (nn' ^. nn_node1_id) .== (n ^. node_id))
returnA -< (n, view nn_node2_id <$> nn)
newtype SourceId = SourceId { _SourceId :: NodeId }
deriving (Show, Eq, Ord)
newtype TargetId = TargetId { _TargetId :: NodeId }
deriving (Show, Eq, Ord)
newtype OwnerId = OwnerId { _OwnerId :: NodeId }
deriving (Show, Eq, Ord)
shareNode :: SourceId -> TargetId -> DBUpdate err Int
shareNode (SourceId sourceId) (TargetId targetId) =
insertNodeNode [ NodeNode sourceId targetId Nothing Nothing ]
-- | Publishes a node, i.e. it creates a relationship between
-- the input node and the target public folder.
-- /NOTE/: Even though the semantic of the relationships it
-- source -> target, by historical reason we store this in the
-- node_node table backwards, i.e. the public folder first as
-- the 'node1_id', and the shared node as the target, so we
-- honour this.
publishNode :: NodePublishPolicy -> SourceId -> TargetId -> DBUpdate err ()
publishNode publishPolicy (SourceId sourceId) (TargetId targetId) =
void $ insertNodeNode [ NodeNode targetId sourceId Nothing (Just $ NNC_publish publishPolicy) ]
-- /NOTE/: Even though the semantic of the relationships it
-- source -> target, by historical reason we store this in the
-- node_node table backwards, i.e. the public folder first as
-- the 'node1_id', and the shared node as the target, so we
-- honour this.
unpublishNode :: SourceId -> TargetId -> DBUpdate err ()
unpublishNode (SourceId sourceId) (TargetId targetId) =
void $ deleteNodeNode targetId sourceId
-- | Pair two nodes together. Typically used to pair
-- together
pairCorpusWithAnnuaire :: SourceId -> TargetId -> DBUpdate err ()
pairCorpusWithAnnuaire (SourceId sourceId) (TargetId targetId) =
void $ insertNodeNode [ NodeNode sourceId targetId Nothing Nothing ]