query_helper 0.0.0 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9748c7b95202e187a62f4e889db450dbd6a406aac6f342d6fb0ae5f532e77f98
4
- data.tar.gz: bc3f05f3093a0730758d05d7fab7485960b4d0dfbd489982e3dbbe996f51fb15
3
+ metadata.gz: 93112cb7974e069da9db1dbf08d4a0f3511076a6991195e669747567f2d2caa7
4
+ data.tar.gz: a242199dcea3eaa67c50792cf0b76cbe9fc9826a81f231649c81120969923cc9
5
5
  SHA512:
6
- metadata.gz: 833e99acbbac34e55b2c99c03977c707792c1e1ed1b86d65a347d2184c01f96894cc5e41c40dab0e81f4b7463511ab63cd1ade1d9f7b36a6b689a9bfcece5df7
7
- data.tar.gz: 73ccb427fc984585951c099fd311ebe5f83ff20d927f6e196b69e0d5f899b7e55e42a027de87424c3e986b94e767aa3faceab106f8aacd07cae1341f6b78c79e
6
+ metadata.gz: ab24ee9a256fbb9282e3adbe79db6436235000a7be0e1f166b570d6b413c412a9f3e2d5b285938630671db1efadd7e4ed9bf13e4aa84326fb28efb060b4d2df7
7
+ data.tar.gz: c08788615159a1da5fb4a218095ca5b7497360dbf7a074522d9f5f46cdffcca96cb40b4b9ba27ce905112b909dae0de42e8682292af0f0840d807b32831456d0
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- query_helper (0.0.0)
4
+ query_helper (0.1.0)
5
5
  activerecord (~> 5.0)
6
6
  activesupport (~> 5.0)
7
7
 
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
  [![TravisCI](https://travis-ci.org/iserve-products/query_helper.svg?branch=master)](https://travis-ci.org/iserve-products/query_helper)
3
3
  [![Gem Version](https://badge.fury.io/rb/query_helper.svg)](https://badge.fury.io/rb/query_helper)
4
4
 
5
- Ruby Gem developed and used at Pattern to paginate, sort, filter, and include associations on sql and active record queries.
5
+ QueryHelper is a ruby gem used to paginate, sort, and filter your API calls in Ruby on Rails using URL params in your HTTP requests. It currently only supports Postgres.
6
6
 
7
7
  ## Installation
8
8
 
@@ -20,278 +20,118 @@ Or install it yourself as:
20
20
 
21
21
  $ gem install query_helper
22
22
 
23
- ## Use
23
+ ## Quick Use
24
24
 
25
- ### SQL Queries
25
+ ### Step 1: Update Base Controller to use the QueryHelper Concern
26
26
 
27
- #### Initialize
28
-
29
- To create a new sql query object run
30
-
31
- ```ruby
32
- QueryHelper::Sql.new(
33
- model:, # required
34
- query:, # required
35
- query_params: , # optional
36
- column_mappings: , # optional
37
- filters: , # optional
38
- sorts: , # optional
39
- page: , # optional
40
- per_page: , # optional
41
- single_record: , # optional, default: false
42
- associations: , # optional
43
- as_json_options: , # optional
44
- run: # optional, default: true
45
- )
46
- ```
47
-
48
- The following arguments are accepted when creating a new objects
49
-
50
- <table>
51
- <tr>
52
- <th>Argument</th>
53
- <th>Description</th>
54
- <th>Example</th>
55
- </tr>
56
- <tr>
57
- <td>model</td>
58
- <td>the model to run the query against</td>
59
- <td>
60
- <pre lang="ruby">
61
- Parent
62
- </pre>
63
- </td>
64
- </tr>
65
- <tr>
66
- <td>query</td>
67
- <td>the custom sql string to be executed</td>
68
- <td>
69
- <pre lang="ruby">
70
- 'select * from parents'
71
- </pre>
72
- </td>
73
- </tr>
74
- <tr>
75
- <td>query_params</td>
76
- <td>a hash of bind variables to be embedded into the sql query</td>
77
- <td>
78
- <pre lang="ruby">
79
- {
80
- age: 20,
81
- name: 'John'
82
- }
83
- </pre>
84
- </td>
85
- </tr>
86
- <tr>
87
- <td>column_mappings</td>
88
- <td>A hash that translates aliases to sql expressions</td>
89
- <td>
90
- <pre lang="ruby">
91
- {
92
- "age" => "parents.age"
93
- "children_count" => {
94
- sql_expression: "count(children.id)",
95
- aggregate: true
96
- }
97
- }
98
- </pre>
99
- </td>
100
- </tr>
101
- <tr>
102
- <td>filters</td>
103
- <td>a list of filters in the form of `{"comparate_alias"=>{"operator_code"=>"value"}}`</td>
104
- <td>
105
- <pre lang="ruby">
106
- {
107
- "age" => { "lt" => 100 },
108
- "children_count" => { "gt" => 0 }
109
- }
110
- </pre>
111
- </td>
112
- </tr>
113
- <tr>
114
- <td>sorts</td>
115
- <td>a comma separated string with a list of sort values</td>
116
- <td>
117
- <pre lang="ruby">
118
- "age:desc,name:asc:lowercase"
119
- </pre>
120
- </td>
121
- </tr>
122
- <tr>
123
- <td>page</td>
124
- <td>the page you want returned</td>
125
- <td>
126
- <pre lang="ruby">
127
- 5
128
- </pre>
129
- </td>
130
- </tr>
131
- <tr>
132
- <td>per_page</td>
133
- <td>the number of results per page</td>
134
- <td>
135
- <pre lang="ruby">
136
- 20
137
- </pre>
138
- </td>
139
- </tr>
140
- <tr>
141
- <td>single_record</td>
142
- <td>whether or not you expect the record to return a single result, if toggled, only the first result will be returned</td>
143
- <td>
144
- <pre lang="ruby">
145
- false
146
- </pre>
147
- </td>
148
- </tr>
149
- <tr>
150
- <td>associations</td>
151
- <td>a list of activerecord associations you'd like included in the payload </td>
152
- <td>
153
- <pre lang="ruby">
154
-
155
- </pre>
156
- </td>
157
- </tr>
158
- <tr>
159
- <td>as_json_options</td>
160
- <td>a list of as_json options you'd like run before returning the payload</td>
161
- <td>
162
- <pre lang="ruby">
163
-
164
- </pre>
165
- </td>
166
- </tr>
167
- <tr>
168
- <td>run</td>
169
- <td>whether or not you'd like to run the query on initilization</td>
170
- <td>
171
- <pre lang="ruby">
172
- false
173
- </pre>
174
- </td>
175
- </tr>
176
- </table>
177
-
178
- ### Active Record Queries
179
-
180
- To run an active record query execute
181
27
  ```ruby
182
- QueryHelper.run_active_record_query(active_record_call, query_helpers, valid_columns, single_record)
28
+ class ApplicationController < ActionController::API
29
+ include QueryHelper::QueryHelperConcern
30
+ before_action :create_query_helper
31
+ end
183
32
  ```
184
- active_record_call: Valid active record syntax (i.e. ```Object.where(state: 'Active')```)
185
- query_helpers: See docs below
186
- valid_columns: Default is []. Pass in an array of columns you want to allow sorting and filtering on.
187
- single_record: Default is false. Pass in true to format payload as a single object instead of a list of objects
188
33
 
34
+ Adding this code creates a `QueryHelper` object preloaded with pagination, filtering, sorting, and association information included in the URL. This object can be accessed by using the `@query_helper` instance variable from within your controllers.
189
35
 
190
- model: A valid ActiveRecord model
191
- query: A string containing your custom SQL query
192
- query_params: a symbolized hash of binds to be included in your SQL query
193
- query_helpers: See docs below
194
- valid_columns: Default is []. Pass in an array of columns you want to allow sorting and filtering on.
195
- single_record: Default is false. Pass in true to format payload as a single object instead of a list of objects
36
+ ### Step 2: Use QueryHelper to run active record and sql queries within your controller
196
37
 
197
- ## Query Helpers
198
- query_helpers is a symbolized hash passed in with information about pagination, associations, filtering and sorting.
199
-
200
- ### Pagination
201
- There are two pagination keys you can pass in as part of the query_helpers objects
38
+ #### Active Record Example
202
39
 
203
40
  ```ruby
204
- {
205
- page: 1,
206
- per_page: 20
207
- }
208
- ```
41
+ class ResourceController < ApplicationController
209
42
 
210
- If at least one of these keys is present, paginated results will be returned.
43
+ def index
44
+ @query_helper.update(
45
+ model: UserNotificationSetting,
46
+ query: "select * from resources r where r.user_id = :user_id",
47
+ bind_variables: { user_id: current_user().id }
48
+ )
211
49
 
212
- ### Sorting
213
- Sorting is controlled by the `sort` key in the query_helpers object
50
+ render json: @query_helper.results()
51
+ end
214
52
 
215
- ```ruby
216
- {
217
- sort: "column_name:sort_direction"
218
- }
53
+ end
219
54
  ```
220
- Sort direction can be either asc or desc. If you wish to lowercase string before sorting include the following:
55
+
56
+ #### Raw SQL Example
57
+
221
58
  ```ruby
222
- {
223
- sort: "name:desc:lowercase"
224
- }
59
+ class ResourceController < ApplicationController
60
+
61
+ def index
62
+ @query_helper.query = Resource.all
63
+ render json: @query_helper.results()
64
+ end
65
+
66
+ end
225
67
  ```
226
68
 
227
- ### Filtering
228
- Filtering is controlled by the `filter` object in the query_helpers hash
69
+ You can also use the `@query_helper.update()` method to update the QueryHelper with an ActiveRecord object
229
70
 
230
71
  ```ruby
231
- {
232
- filter: {
233
- "column_1" => {
234
- "gte" => 20,
235
- "lt" => 40
236
- },
237
- "column_2" => {
238
- "eql" => "my_string"
239
- },
240
- "column_3" => {
241
- "like" => "my_string%"
242
- },
243
- "column_4" => {
244
- "in" => "item1,item2,item3"
245
- }
246
- }
72
+ @query_helper.update(
73
+ query: Resource.all
74
+ )
247
75
  ```
248
76
 
249
- The following operator codes are valid
77
+ ### Step 3: Paginate, Sort, Filter, and Include Associations using URL params
250
78
 
251
- ```
252
- “gte”: >=
253
- “lte”: <=
254
- “gt”: >
255
- “lt”: <
256
- “eql”: =
257
- “noteql”: !=
258
- "like": like
259
- “in”: in
260
- “notin” not in
261
- “null”: “is null” or “is not null” (pass in true or false as the value)
262
- ```
79
+ #### Pagination
263
80
 
264
- ### Associations
81
+ `page=1`
265
82
 
266
- To include associated objects in the payload, pass in the following as part of the query_helpers hash:
83
+ `per_page=20`
267
84
 
268
- ```ruby
269
- {
270
- include: ['associated_object_1', 'associated_object_2']
271
- }
272
- ```
85
+ `http://www.example.com/resources?page=1&per_page=25`
273
86
 
274
- ### Example
87
+ #### Sorting
88
+
89
+ `sort=column:direction`
90
+
91
+ Single Sort: `http://www.example.com/resources?sort=resource_name:desc`
92
+
93
+ Multiple Sorts: `http://www.example.com/resources?sort=resource_name:desc,resource_age:asc`
94
+
95
+ Lowercase Sort: `http://www.example.com/resources?sort=resource_name:desc:lowercase`
96
+
97
+ #### Filtering
98
+
99
+ `filter[column][operator_code]=value`
100
+
101
+ Single Filter: `http://www.example.com/resources?filter[resource_age][gt]=50`
102
+
103
+ Multiple Filters: `http://www.example.com/resources?filter[resource_age][gt]=50&[resource_name][eql]=banana_resource`
104
+
105
+ Operator Code | SQL Operator
106
+ --- | ---
107
+ gte | >=
108
+ lte | <=
109
+ gt | >
110
+ lt | <
111
+ eql | =
112
+ noteql | !=
113
+ like | like
114
+ in | in
115
+ notin | not in
116
+ null | is null *or* is not null
117
+
118
+ Note: For the null operator code, toggle *is null* operator with true and *is not null* operator with false
119
+
120
+ #### Associations
121
+
122
+ Include ActiveRecord associations in the payload. The association must be defined in the model.
123
+
124
+ `include=association`
125
+
126
+ Single Association: `http://www.example.com/resources?include=child_resource`
127
+
128
+ Multiple Associations: `http://www.example.com/resources?include[]=child_resource&include[]=parent_resource`
275
129
 
276
- The following is an example of a query_helpers object that can be passed into the sql and active record methods
277
130
 
278
- ```ruby
279
- query_helpers = {
280
- page: 1,
281
- per_page: 20,
282
- sort: "name:desc"
283
- include: ["child"]
284
- filter: {
285
- "id" => {
286
- "gte" => 20,
287
- "lt" => 40
288
- }
289
- }
290
- ```
291
131
 
292
132
  ## Payload Formats
293
133
 
294
- The QueryHelper gem will return results in one of three formats
134
+ The QueryHelper gem will return the following payload
295
135
 
296
136
  ### Paginated List Payload
297
137
  ```json
@@ -330,30 +170,39 @@ The QueryHelper gem will return results in one of three formats
330
170
  }
331
171
  ```
332
172
 
333
- ### List Payload
334
- ```json
335
- {
336
- "data": [
337
- {
338
- "id": 1,
339
- "attribute_1": "string_attribute",
340
- "attribute_2": 12345,
341
- "attribute_3": 0.3423212
342
- },
343
- {
344
- "id": 2,
345
- "attribute_1": "string_attribute",
346
- "attribute_2": 12345,
347
- "attribute_3": 0.3423212
348
- },
349
- {
350
- "id": 3,
351
- "attribute_1": "string_attribute",
352
- "attribute_2": 12345,
353
- "attribute_3": 0.3423212
354
- },
355
- ]
356
- }
173
+ ## Advanced Options
174
+
175
+ ### Associations
176
+
177
+ You can preload additional and include additional associations in your payload besides what's defined in the `include` url parameter.
178
+
179
+ ```ruby
180
+ @query_helper.update(
181
+ associations: ['association1']
182
+ )
183
+ ```
184
+
185
+ ### as_json options
186
+
187
+ You can pass in additional as_json options to be included in the payload.
188
+
189
+ ```ruby
190
+ @query_helper.update(
191
+ as_json_options: { methods: [:last_ran_at] }
192
+ )
193
+ ```
194
+
195
+ ### Single Record Queries
196
+ If you only want to return a single result, but still want to be able to use some of the other functionality of QueryHelper, you can set `single_record` to true in the QueryHelper object.
197
+
198
+ ```ruby
199
+ @query_helper.single_record = true
200
+ ```
201
+ or
202
+ ```ruby
203
+ @query_helper.update(
204
+ single_record: true
205
+ )
357
206
  ```
358
207
 
359
208
  ### Single Record Payload
@@ -370,7 +219,7 @@ The QueryHelper gem will return results in one of three formats
370
219
 
371
220
  ## Contributing
372
221
 
373
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/query_helper. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
222
+ Bug reports and pull requests are welcome on GitHub at https://github.com/iserve_products/query_helper. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
374
223
 
375
224
  ## License
376
225
 
@@ -70,10 +70,13 @@ class QueryHelper
70
70
 
71
71
  def mofify_criterion
72
72
  # lowercase strings for comparison
73
- @criterion.downcase! if criterion.class == String && criterion.scan(/[a-zA-Z]/).any?
73
+ @criterion.downcase! if @criterion.class == String && @criterion.scan(/[a-zA-Z]/).any?
74
74
 
75
75
  # turn the criterion into an array for in and notin comparisons
76
- @criterion = criterion.split(",") if ["in", "notin"].include?(operator_code) && criterion.class == String
76
+ @criterion = @criterion.split(",") if ["in", "notin"].include?(@operator_code) && @criterion.class == String
77
+
78
+ # Add wildcards for like comparisons
79
+ @criterion = "%#{@criterion}%" if @operator_code == "like"
77
80
  end
78
81
 
79
82
  def modify_comparate
@@ -1,5 +1,4 @@
1
1
  require 'active_support/concern'
2
- require "query_helper/sql_filter"
3
2
 
4
3
  class QueryHelper
5
4
  module QueryHelperConcern
@@ -24,7 +23,7 @@ class QueryHelper
24
23
  end
25
24
 
26
25
  def create_query_helper_associations
27
-
26
+ QueryHelper::Associations.process_association_params(params[:include])
28
27
  end
29
28
 
30
29
  def query_helper_params
@@ -10,7 +10,8 @@ class QueryHelper
10
10
  where_clauses: nil,
11
11
  having_clauses: nil,
12
12
  order_by_clauses: nil,
13
- include_limit_clause: false
13
+ include_limit_clause: false,
14
+ additional_select_clauses: []
14
15
  )
15
16
  @parser = SqlParser.new(sql)
16
17
  @sql = @parser.sql.dup
@@ -18,23 +19,23 @@ class QueryHelper
18
19
  @having_clauses = having_clauses
19
20
  @order_by_clauses = order_by_clauses
20
21
  @include_limit_clause = include_limit_clause
22
+ @additional_select_clauses = additional_select_clauses
21
23
  end
22
24
 
23
25
  def build
24
26
  insert_having_clauses()
25
27
  insert_where_clauses()
26
- insert_total_count_select_clause()
28
+ insert_select_clauses()
27
29
  insert_order_by_and_limit_clause()
28
30
  @sql.squish
29
31
  end
30
32
 
31
33
  private
32
34
 
33
- def insert_total_count_select_clause
34
- return unless @include_limit_clause
35
- total_count_clause = " ,count(*) over () as _query_full_count "
36
- @sql.insert(@parser.insert_select_index, total_count_clause)
37
- # Potentially update parser here
35
+ def insert_select_clauses
36
+ total_count_clause = "count(*) over () as _query_full_count"
37
+ @additional_select_clauses << total_count_clause if @include_limit_clause
38
+ @sql.insert(@parser.insert_select_index, " , #{@additional_select_clauses.join(", ")} ") if @additional_select_clauses.length > 0
38
39
  end
39
40
 
40
41
  def insert_where_clauses
@@ -3,11 +3,12 @@ require "query_helper/invalid_query_error"
3
3
  class QueryHelper
4
4
  class SqlSort
5
5
 
6
- attr_accessor :column_maps
6
+ attr_accessor :column_maps, :select_strings
7
7
 
8
8
  def initialize(sort_string: "", column_maps: [])
9
9
  @sort_string = sort_string
10
10
  @column_maps = column_maps
11
+ @select_strings = []
11
12
  end
12
13
 
13
14
  def parse_sort_string
@@ -38,6 +39,8 @@ class QueryHelper
38
39
  case modifier
39
40
  when "lowercase"
40
41
  sql_expression = "lower(#{sql_expression})"
42
+ # When select distincts are used, the order by clause must be included in the select clause
43
+ @select_strings << sql_expression
41
44
  end
42
45
 
43
46
  sql_strings << "#{sql_expression} #{direction}"
@@ -1,3 +1,3 @@
1
1
  class QueryHelper
2
- VERSION = "0.0.0"
2
+ VERSION = "0.1.0"
3
3
  end
data/lib/query_helper.rb CHANGED
@@ -24,7 +24,7 @@ class QueryHelper
24
24
  page: nil, # define the page you want returned
25
25
  per_page: nil, # define how many results you want per page
26
26
  single_record: false, # whether or not you expect the record to return a single result, if toggled, only the first result will be returned
27
- associations: nil, # a list of activerecord associations you'd like included in the payload
27
+ associations: [], # a list of activerecord associations you'd like included in the payload
28
28
  as_json_options: nil, # a list of as_json options you'd like run before returning the payload
29
29
  custom_mappings: {}, # custom keyword => sql_expression mappings
30
30
  api_payload: false # Return the paginated payload or simply return the result array
@@ -52,10 +52,24 @@ class QueryHelper
52
52
  end
53
53
  end
54
54
 
55
- def update_query(query: nil, model:nil, bind_variables: {})
55
+ def update(
56
+ query: nil,
57
+ model: nil,
58
+ bind_variables: {},
59
+ filters: [],
60
+ associations: [],
61
+ as_json_options: nil,
62
+ single_record: nil,
63
+ custom_mappings: nil
64
+ )
56
65
  @model = model if model
57
66
  @query = query if query
58
67
  @bind_variables.merge!(bind_variables)
68
+ filters.each{ |f| add_filter(**f) }
69
+ @associations = @associations | associations
70
+ @single_record = single_record if single_record
71
+ @as_json_options = as_json_options if as_json_options
72
+ @custom_mappings = custom_mappings if custom_mappings
59
73
  end
60
74
 
61
75
  def add_filter(operator_code:, criterion:, comparate:)
@@ -68,6 +82,7 @@ class QueryHelper
68
82
 
69
83
  # Create column maps to be used by the filter and sort objects
70
84
  column_maps = create_column_maps()
85
+
71
86
  @sql_filter.column_maps = column_maps
72
87
  @sql_sort.column_maps = column_maps
73
88
 
@@ -83,7 +98,8 @@ class QueryHelper
83
98
  where_clauses: @sql_filter.where_clauses,
84
99
  having_clauses: @sql_filter.having_clauses,
85
100
  order_by_clauses: @sql_sort.parse_sort_string,
86
- include_limit_clause: @page && @per_page ? true : false
101
+ include_limit_clause: @page && @per_page ? true : false,
102
+ additional_select_clauses: @sql_sort.select_strings
87
103
  )
88
104
  @executed_query = manipulator.build()
89
105
  @results = @model.find_by_sql([@executed_query, @bind_variables]) # Execute Sql Query
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: query_helper
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.0
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Evan McDaniel
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-07-23 00:00:00.000000000 Z
11
+ date: 2019-08-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler