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
open Base
module Filter = struct
type op = Eq | Like [@@deriving show, eq, sexp, yojson]
type criterion = { key : string; value : string; op : op }
[@@deriving show, eq, sexp, yojson]
type t = And of t list | Or of t list | C of criterion
[@@deriving show, eq, sexp, yojson]
end
module Sort = struct
type criterion = Asc of string | Desc of string
[@@deriving show, eq, sexp, yojson]
type t = criterion list [@@deriving show, eq, sexp, yojson]
let criterion_value = function Asc value -> value | Desc value -> value
end
module Page = struct
type t = {
limit : int option; [@sexp.option]
offset : int option; [@sexp.option]
}
[@@deriving show, eq, sexp, yojson]
let empty = { limit = None; offset = None }
let set_limit limit page = { page with limit = Some limit }
let set_offset offset page = { page with offset = Some offset }
let get_limit page = page.limit
let get_offset page = page.offset
let of_string str =
if String.equal str "" then Ok empty
else
let sexp = Sexplib.Sexp.of_string str in
Ok (t_of_sexp sexp)
let to_string query =
let sexp = query |> sexp_of_t in
Sexplib.Sexp.to_string sexp
end
type t = {
filter : Filter.t option; [@sexp.option]
sort : Sort.t option; [@sexp.option]
page : Page.t;
}
[@@deriving show, eq, sexp, yojson]
let get_page query = query.page
let get_limit query = query.page.limit
let get_offset query = query.page.offset
module Sql = struct
let is_field_whitelisted whitelist field =
whitelist |> List.find ~f:(String.equal field) |> Option.is_some
let limit limit = ("LIMIT ?", [ Int.to_string limit ])
let offset offset = ("OFFSET ?", [ Int.to_string offset ])
let sort whitelist sort =
let sorts =
sort
|> List.filter ~f:(fun criterion ->
criterion |> Sort.criterion_value |> is_field_whitelisted whitelist)
|> List.map ~f:(function
| Sort.Asc value -> Printf.sprintf "%s ASC" value
| Sort.Desc value -> Printf.sprintf "%s DESC" value)
|> String.concat ~sep:", "
in
if String.is_empty sorts then "" else Printf.sprintf "ORDER BY %s" sorts
let filter_criterion_to_string criterion =
let op_string =
Filter.(match criterion.op with Eq -> "=" | Like -> "LIKE")
in
Printf.sprintf "%s %s ?" criterion.key op_string
let is_filter_whitelisted whitelist filter =
match filter with
| Filter.C criterion ->
is_field_whitelisted whitelist Filter.(criterion.key)
| _ -> true
let filter whitelist filter =
let values = ref [] in
let rec to_string filter =
Filter.(
match filter with
| C criterion ->
values := List.concat [ !values; [ criterion.value ] ];
filter_criterion_to_string criterion
| And [] -> ""
| Or [] -> ""
| And filters ->
let whitelisted_filters =
filters |> List.filter ~f:(is_filter_whitelisted whitelist)
in
let criterions_string =
whitelisted_filters |> List.map ~f:to_string
|> String.concat ~sep:" AND "
in
if List.length whitelisted_filters > 1 then
Printf.sprintf "(%s)" criterions_string
else Printf.sprintf "%s" criterions_string
| Or filters ->
let whitelisted_filters =
filters |> List.filter ~f:(is_filter_whitelisted whitelist)
in
let criterions_string =
whitelisted_filters |> List.map ~f:to_string
|> String.concat ~sep:" OR "
in
if List.length whitelisted_filters > 1 then
Printf.sprintf "(%s)" criterions_string
else Printf.sprintf "%s" criterions_string)
in
let result = to_string filter in
let result =
if String.is_empty result then "" else Printf.sprintf "WHERE %s" result
in
(result, !values)
let to_fragments field_whitelist query =
let filter_qs, filter_values =
query.filter
|> Option.map ~f:(filter field_whitelist)
|> Option.value ~default:("", [])
in
let sort_qs =
query.sort
|> Option.map ~f:(sort field_whitelist)
|> Option.value ~default:""
in
let limit_fragment = get_limit query |> Option.map ~f:limit in
let offset_fragment = get_offset query |> Option.map ~f:offset in
let , =
Option.merge limit_fragment offset_fragment
~f:(fun (limit_query, limit_value) (offset_query, offset_value) ->
( limit_query ^ " " ^ offset_query,
List.concat [ limit_value; offset_value ] ))
|> Option.value ~default:("", [])
in
( filter_qs,
sort_qs,
pagination_qs,
List.concat [ filter_values; pagination_values ] )
let to_string field_whitelist query =
let filter_fragment, sort_fragment, , values =
to_fragments field_whitelist query
in
let qs =
List.filter
~f:(fun str -> not (String.is_empty str))
[ filter_fragment; sort_fragment; pagination_fragment ]
|> String.concat ~sep:" "
in
(qs, values)
end
let of_string str =
if String.equal str "" then
Ok { filter = None; sort = None; page = { limit = None; offset = None } }
else
let sexp = Sexplib.Sexp.of_string str in
Ok (t_of_sexp sexp)
let to_string query =
let sexp = query |> sexp_of_t in
Sexplib.Sexp.to_string sexp
let to_sql = Sql.to_string
let to_sql_fragments = Sql.to_fragments
let empty =
{ filter = None; sort = None; page = { limit = None; offset = None } }
let set_filter filter query = { query with filter = Some filter }
let set_filter_and criterion query =
let open Filter in
let new_filter =
match query.filter with
| Some filter -> And (List.append [ filter ] [ C criterion ])
| None -> C criterion
in
{ query with filter = Some new_filter }
let set_sort sort query = { query with sort = Some sort }
let set_limit limit query =
let page = { query.page with limit = Some limit } in
{ query with page }
let set_offset offset query =
let page = { query.page with offset = Some offset } in
{ query with page }