package sihl

  1. Overview
  2. Docs
Legend:
Page
Library
Module
Module type
Parameter
Class
Class type
Source

Source file ql.ml

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
open Sexplib.Std

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_opt (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 (fun criterion ->
             criterion |> Sort.criterion_value |> is_field_whitelisted whitelist)
      |> List.map (function
             | Sort.Asc value -> Printf.sprintf "%s ASC" value
             | Sort.Desc value -> Printf.sprintf "%s DESC" value)
      |> String.concat ", "
    in
    if String.equal "" 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 (is_filter_whitelisted whitelist)
          in
          let criterions_string =
            whitelisted_filters |> List.map to_string |> String.concat " 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 (is_filter_whitelisted whitelist)
          in
          let criterions_string =
            whitelisted_filters |> List.map to_string |> String.concat " 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.equal "" 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 (filter field_whitelist) |> Option.value ~default:("", [])
    in
    let sort_qs =
      query.sort |> Option.map (sort field_whitelist) |> Option.value ~default:""
    in
    let limit_fragment = get_limit query |> Option.map limit in
    let offset_fragment = get_offset query |> Option.map offset in
    let pagination_qs, pagination_values =
      (match limit_fragment, offset_fragment with
      | Some (limit_query, limit_value), Some (offset_query, offset_value) ->
        Some (limit_query ^ " " ^ offset_query, List.concat [ limit_value; offset_value ])
      | _ -> None)
      |> 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, pagination_fragment, values =
      to_fragments field_whitelist query
    in
    let qs =
      List.filter
        (fun str -> not (String.equal "" str))
        [ filter_fragment; sort_fragment; pagination_fragment ]
      |> String.concat " "
    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 }
;;
OCaml

Innovation. Community. Security.