-
Notifications
You must be signed in to change notification settings - Fork 201
/
Copy pathFetchRelated.hs
249 lines (236 loc) · 12.1 KB
/
FetchRelated.hs
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
{-# LANGUAGE TypeFamilies, DataKinds, MultiParamTypeClasses, PolyKinds, TypeApplications, ScopedTypeVariables, ConstraintKinds, TypeOperators, GADTs, UndecidableInstances, StandaloneDeriving, FunctionalDependencies, FlexibleContexts, AllowAmbiguousTypes #-}
{-|
Module: IHP.FetchRelated
Description: Provides fetchRelated, collectionFetchRelated, etc.
Copyright: (c) digitally induced GmbH, 2020
This modules provides helper functions to access relationshops for a model.
See https://ihp.digitallyinduced.com/Guide/relationships.html for some examples.
-}
module IHP.FetchRelated (fetchRelated, collectionFetchRelated, collectionFetchRelatedOrNothing, fetchRelatedOrNothing, maybeFetchRelatedOrNothing) where
import IHP.Prelude
import Database.PostgreSQL.Simple.ToField
import qualified Database.PostgreSQL.Simple as PG
import IHP.ModelSupport (Include, Id', PrimaryKey, GetModelByTableName, Table)
import IHP.QueryBuilder
import IHP.Fetch
-- | This class provides the collectionFetchRelated function
--
-- This function is provided by this class as we have to deal with two cases:
--
-- 1. the related field is a id, e.g. like the company ids in @users |> collectionFetchRelated #companyId@
-- 2. the related field is a query builder, e.g. in @posts |> collectionFetchRelated #comments@
class CollectionFetchRelated relatedFieldValue relatedModel where
collectionFetchRelated :: forall model relatedField. (
?modelContext :: ModelContext,
HasField relatedField model relatedFieldValue,
UpdateField relatedField model (Include relatedField model) relatedFieldValue (FetchResult relatedFieldValue relatedModel),
Fetchable relatedFieldValue relatedModel,
KnownSymbol (GetTableName relatedModel),
PG.FromRow relatedModel,
KnownSymbol relatedField
) => Proxy relatedField -> [model] -> IO [Include relatedField model]
-- | This class provides the collectionFetchRelatedOrNothing function
--
-- This function is provided by this class as we have to deal with two cases:
--
-- 1. the related field is an id, e.g. like the company ids in @users |> collectionFetchRelated #companyId@
-- 2. the related field is a query builder, e.g. in @posts |> collectionFetchRelated #comments@
class CollectionFetchRelatedOrNothing relatedFieldValue relatedModel where
collectionFetchRelatedOrNothing :: forall model relatedField. (
?modelContext :: ModelContext,
HasField relatedField model (Maybe relatedFieldValue),
UpdateField relatedField model (Include relatedField model) (Maybe relatedFieldValue) (Maybe (FetchResult relatedFieldValue relatedModel)),
Fetchable relatedFieldValue relatedModel,
KnownSymbol (GetTableName relatedModel),
PG.FromRow relatedModel,
KnownSymbol relatedField
) => Proxy relatedField -> [model] -> IO [Include relatedField model]
-- | Provides collectionFetchRelated for ids, e.g. @collectionFetchRelated #companyId@
--
-- When we want to fetch all the users with their companies, we can use collectionFetchRelated like this:
--
-- > users <- query @User
-- > |> fetch
-- > >>= collectionFetchRelated #companyId
--
-- This will query all users with their company. The type of @users@ is @[Include "companyId" User]@.
--
-- This example will trigger only two SQL queries:
--
-- > SELECT * FROM users
-- > SELECT * FROM companies WHERE id IN (?)
instance (
Eq (PrimaryKey tableName)
, ToField (PrimaryKey tableName)
, Show (PrimaryKey tableName)
, HasField "id" relatedModel (Id' tableName)
, relatedModel ~ GetModelByTableName (GetTableName relatedModel)
, GetTableName relatedModel ~ tableName
, Table relatedModel
) => CollectionFetchRelated (Id' tableName) relatedModel where
collectionFetchRelated :: forall model relatedField. (
?modelContext :: ModelContext,
HasField relatedField model (Id' tableName),
UpdateField relatedField model (Include relatedField model) (Id' tableName) (FetchResult (Id' tableName) relatedModel),
Fetchable (Id' tableName) relatedModel,
KnownSymbol (GetTableName relatedModel),
PG.FromRow relatedModel,
KnownSymbol relatedField,
Table relatedModel
) => Proxy relatedField -> [model] -> IO [Include relatedField model]
collectionFetchRelated relatedField model = do
relatedModels :: [relatedModel] <- query @relatedModel |> filterWhereIdIn (map (getField @relatedField) model) |> fetch
let
assignRelated :: model -> Include relatedField model
assignRelated model =
let
relatedModel :: relatedModel
relatedModel = case find (\r -> r.id == targetForeignKey) relatedModels of
Just m -> m
Nothing -> error ("Could not find record with id = " <> show targetForeignKey <> " in result set. Looks like the foreign key is pointing to a non existing record")
targetForeignKey = (getField @relatedField model :: Id' tableName)
in
updateField @relatedField relatedModel model
let
result :: [Include relatedField model]
result = map assignRelated model
pure result
-- | Provides collectionFetchRelatedOrNothing for nullable ids, e.g. @collectionFetchRelatedOrNothing #companyId@
--
-- When we want to fetch all the users with their companies, we can use collectionFetchRelatedOrNothing like this:
--
-- > users <- query @User
-- > |> fetch
-- > >>= collectionFetchRelatedOrNothing #companyId
--
-- This will query all users with their company. The type of @users@ is @[Include "companyId" User]@.
--
-- This example will trigger only two SQL queries:
--
-- > SELECT * FROM users
-- > SELECT * FROM companies WHERE id IN (?)
instance (
Eq (PrimaryKey tableName)
, ToField (PrimaryKey tableName)
, HasField "id" relatedModel (Id' tableName)
, relatedModel ~ GetModelByTableName (GetTableName relatedModel)
, GetTableName relatedModel ~ tableName
, Table relatedModel
) => CollectionFetchRelatedOrNothing (Id' tableName) relatedModel where
collectionFetchRelatedOrNothing :: forall model relatedField. (
?modelContext :: ModelContext,
HasField relatedField model (Maybe (Id' tableName)),
UpdateField relatedField model (Include relatedField model) (Maybe (Id' tableName)) (Maybe (FetchResult (Id' tableName) relatedModel)),
Fetchable (Id' tableName) relatedModel,
KnownSymbol (GetTableName relatedModel),
PG.FromRow relatedModel,
KnownSymbol relatedField
) => Proxy relatedField -> [model] -> IO [Include relatedField model]
collectionFetchRelatedOrNothing relatedField model = do
relatedModels :: [relatedModel] <- query @relatedModel |> filterWhereIdIn (mapMaybe (getField @relatedField) model) |> fetch
let
assignRelated :: model -> Include relatedField model
assignRelated model =
let
relatedModel :: Maybe (FetchResult (Id' tableName) relatedModel)
relatedModel = find (\r -> Just r.id == targetForeignKey) relatedModels
targetForeignKey = (getField @relatedField model :: Maybe (Id' tableName))
in
updateField @relatedField relatedModel model
let
result :: [Include relatedField model]
result = map assignRelated model
pure result
-- | Provides collectionFetchRelated for QueryBuilder's, e.g. @collectionFetchRelated #comments@
--
-- When we want to fetch all the comments for a list of posts, we can use collectionFetchRelated like this:
--
-- > posts <- query @Post
-- > |> fetch
-- > >>= collectionFetchRelated #comments
--
-- This will query all posts with their comments. The type of @posts@ is @[Include "comments" Post]@.
--
-- When fetching query builders, currently the implementation is not very efficient. E.g. given 10 Posts above, it will run 10 queries to fetch the comments. We should optimise this behavior in the future.
instance (relatedModel ~ GetModelByTableName relatedTable, Table relatedModel) => CollectionFetchRelated (QueryBuilder relatedTable) relatedModel where
collectionFetchRelated :: forall model relatedField. (
?modelContext :: ModelContext,
HasField relatedField model (QueryBuilder relatedTable),
UpdateField relatedField model (Include relatedField model) (QueryBuilder relatedTable) (FetchResult (QueryBuilder relatedTable) relatedModel),
Fetchable (QueryBuilder relatedTable) relatedModel,
KnownSymbol (GetTableName relatedModel),
PG.FromRow relatedModel,
KnownSymbol relatedField
) => Proxy relatedField -> [model] -> IO [Include relatedField model]
collectionFetchRelated relatedField models = do
let fetchRelated model = do
let queryBuilder :: QueryBuilder relatedTable = getField @relatedField model
result :: [relatedModel] <- fetch queryBuilder
pure (updateField @relatedField result model)
mapM fetchRelated models
-- Fetches a related record
--
-- Given a specific post, we can fetch the post and all its comments like this:
--
-- > let postId :: Id Post = ...
-- >
-- > post <- fetch postId
-- > >>= fetchRelated #comments
--
-- This Haskell code will trigger the following SQL queries to be executed:
--
-- > SELECT posts.* FROM posts WHERE id = ? LIMIT 1
-- > SELECT comments.* FROM comments WHERE post_id = ?
--
-- In the view we can just access the comments like this:
--
-- > [hsx|
-- > <h1>{post.title}</h1>
-- > <h2>Comments:</h2>
-- > {post.comments}
-- > |]
--
-- The @post.comments@ returns a list of the comments belonging to the post.
-- The type of post is @Include "comments"@ Post instead of the usual @Post@. This way the state of a fetched nested resource is tracked at the type level.
--
fetchRelated :: forall model field fieldValue fetchModel. (
?modelContext :: ModelContext,
UpdateField field model (Include field model) fieldValue (FetchResult fieldValue fetchModel),
HasField field model fieldValue,
PG.FromRow fetchModel,
KnownSymbol (GetTableName fetchModel),
Fetchable fieldValue fetchModel,
Table fetchModel
) => Proxy field -> model -> IO (Include field model)
fetchRelated relatedField model = do
result :: FetchResult fieldValue fetchModel <- fetch ((getField @field model) :: fieldValue)
let model' = updateField @field result model
pure model'
{-# INLINE fetchRelated #-}
fetchRelatedOrNothing :: forall model field fieldValue fetchModel. (
?modelContext :: ModelContext,
UpdateField field model (Include field model) (Maybe fieldValue) (Maybe (FetchResult fieldValue fetchModel)),
HasField field model (Maybe fieldValue),
PG.FromRow fetchModel,
KnownSymbol (GetTableName fetchModel),
Fetchable fieldValue fetchModel,
Table fetchModel
) => Proxy field -> model -> IO (Include field model)
fetchRelatedOrNothing relatedField model = do
result :: Maybe (FetchResult fieldValue fetchModel) <- case getField @field model of
Just fieldValue -> Just <$> fetch fieldValue
Nothing -> pure Nothing
let model' = updateField @field result model
pure model'
{-# INLINE fetchRelatedOrNothing #-}
maybeFetchRelatedOrNothing :: forall model field fieldValue fetchModel. (
?modelContext :: ModelContext,
UpdateField field model (Include field model) (Maybe fieldValue) (Maybe (FetchResult fieldValue fetchModel)),
HasField field model (Maybe fieldValue),
PG.FromRow fetchModel,
KnownSymbol (GetTableName fetchModel),
Fetchable fieldValue fetchModel,
Table fetchModel
) => Proxy field -> Maybe model -> IO (Maybe (Include field model))
maybeFetchRelatedOrNothing relatedField = maybe (pure Nothing) (\q -> fetchRelatedOrNothing relatedField q >>= pure . Just)
{-# INLINE maybeFetchRelatedOrNothing #-}