like_query 0.0.6 → 0.0.8

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: 84fed18b7e4d5122c2d760bdf76c27bcc7094d7e44a140e0b42d8dff1d574827
4
- data.tar.gz: adb485617d123592abd58140a086073da2745eef15d25a4c84a40635ae1e92f7
3
+ metadata.gz: c46b541528f87a7bd078c47eaa7bc264503ff6b1c333af0caf18aacb2beea24c
4
+ data.tar.gz: 8d5d867c0707f5cfe827a4d55082434d5e02bec45528f04737a8927746388fcd
5
5
  SHA512:
6
- metadata.gz: 5e7b363e0409f55048163956d485b2b93668eddb3f1de4b49daca8aaffafd2f9866c4a3dfc6abf6b12fecbfdae5e190fe69e8265c162fac97b7e3aff0bfb858d
7
- data.tar.gz: 4b5a6c0aff2b9a4c1f7c7fa2f08da962e6e4937146a351f8a2c57d8946f2880051a4b1d209a997fbdf4f0eac0e208dcf379603530bc0413b298e8ee8ec6f39e8
6
+ metadata.gz: aaf145febd6f66ee1549dd8d2d5b5285d1c191f6e5759519ec5053eb88a6055c087411ab3d4962afb282d3c208f439f9a7fad5f852797a3466e93c22238f6aba
7
+ data.tar.gz: 7097e44705ad35add8233002e3db0e0b20330525197a3dcc4d15070e43d38f03221c3f2e9902e8677ee6e273e68e6eaf4490d6bc825ce82fdce2722d0301dd2d
data/README.md CHANGED
@@ -1,10 +1,13 @@
1
1
  # like_query
2
2
 
3
- For my customers apps, newly built with turbo, search queries are mostly for two purposes:
3
+ For my clients' newly built applications with Turbo, search queries mostly serve two purposes:
4
4
 
5
- Index view callable by a url like `/customers?find=müller screw` or a javascript dropdown on the front, in our case built with svelte, which receives a json and renders a table in a dropdown.
5
+ - Index view callable by a url like `/customers?find=müller screw`
6
+ - javascript component / dropdown on the front, in our case built with svelte, which receives a json and renders a table in a dropdown.
6
7
 
7
- This query generator is built for this two purposes.
8
+ This query generator is built for these two purposes
9
+
10
+ Modules like one of the svelte components mentioned above have a total response time (from pressing a key until the result is rendered) of about 60 msec, while with turbo the same time is mostly around 140-160 msec. The gem itself, from querying the database to producing a hash, has a time of about 3 msec. These results are for small data sets (e.g. 30 records found).
8
11
 
9
12
  ## Installation
10
13
 
@@ -14,18 +17,28 @@ add
14
17
 
15
18
  to Gemfile
16
19
 
17
- this adds the methods `#like` and `#like_result` to all models.
20
+ this adds the methods `#like` and `#generate_hash` to all models.
21
+
22
+ **Config**
23
+
24
+ You can set a default limit by
25
+
26
+ ```ruby
27
+ config.x.like_query.limit = 20
28
+ ```
29
+
30
+ This can be overriden by calling the methods
18
31
 
19
32
  ## Usage
20
33
 
21
- **.like**
34
+ **#like**
22
35
 
23
36
  ```ruby
24
37
  customer = Customer.create(name: 'Ambühl')
25
- art1 = Article.create(name: 'first', number: '01', customer: customer)
38
+ Article.create(name: 'first', number: '01', customer: customer)
26
39
 
27
40
  Article.like('fir', :name, :number)
28
- # => would find art1
41
+ # => <Article:0x00000001067107a8 id: ...>
29
42
  # => searches by: "where name like '%fir%' or number like '%fir%'"
30
43
  # => queries are built with Article.arel_table[:name].matches('%fir%')
31
44
 
@@ -35,75 +48,174 @@ Article.like(['fir', 'ambühl'], :name, :number, customer: :name)
35
48
  # => would also find art1
36
49
  ```
37
50
 
38
- **.like_result**
39
-
40
- returns a hash that can easily be transformed by `#to_json` for a javascript-frontend, by example
51
+ **#generate_hash**
41
52
 
42
- can only be chained behind `#like`.
53
+ returns a hash that can easily be transformed by `#to_json` for a javascript frontend, for example
43
54
 
44
55
  ```ruby
45
56
  customer = Customer.create(name: 'Ambühl')
46
57
  art1 = Article.create(name: 'first', number: '01', customer: customer)
47
58
 
48
- Article.like('fir', :name).like_result(limit: 10)
59
+ Article.like('fir', :name).generate_hash(limit: 10)
49
60
  # returns:
50
- {
51
- :data=>[
52
- {
53
- :attributes=>[[:name, "first"]],
54
- :id=>1}],
55
- :length=>1,
56
- :overflow=>false,
57
- :columns_count=>1,
58
- :sub_records_columns_count=>0
61
+ {
62
+ data:
63
+ [
64
+ {
65
+ values: ["Müller"],
66
+ attributes: {"customer.name": "Müller"},
67
+ id: 1,
68
+ model: "Article"
69
+ },
70
+ ...
71
+ ],
72
+ length: 4,
73
+ overflow: true,
74
+ columns_count: 2,
75
+ sub_records_columns_count: 0,
76
+ image: false,
77
+ time: 18.282996
59
78
  }
60
79
 
61
- Article.like('fir', :name).like_result( :number, limit: 10)
80
+ Article.like('fir', :name).generate_hash(:number, limit: 10)
62
81
  # would query like the above example: Search scope is only :name
63
82
  # but would return article-number instead of article-name inside the data block
64
83
  ```
65
84
 
66
- `#like_result` uses `LikeQuery::Collect`, functionality is the same.
85
+ `#generate_hash` uses `LikeQuery::Collect`, functionality is the same.
67
86
 
68
- **Class LikeQuery::Collect**
87
+ **Class LikeQuery::Collect**
69
88
 
70
89
  ```ruby
71
- cust = Customer.create(name: 'Müller')
72
- 20.times {Article.create(name: 'any-article', customer: cust)}
90
+ cust = Customer.create(name: 'cust')
91
+ Article.create(name: 'screw', customer: cust)
73
92
 
74
93
  c = LikeQuery::Collect.new(4)
75
94
  # => 4 is the limit
76
95
 
77
- c.receive { Article.like('any-art', :name) }
78
- # => would add 4 articles to the result hash because of limit
79
-
80
- c.receive { Customer.like('any-art', :name, image: :image_column, articles: :name) }
81
- # => limit is already exhausted: does nothing
82
- # => otherwise it would add Customers to the result hash
83
-
96
+ c.set_schema(Customer, :name)
97
+ c.collect(parent: :customer) { Article.like('screw', :name) }
98
+ c.generate_json
84
99
  c.result
85
- # => would return anything like (this output is from different code!!):
100
+
101
+ # =>
86
102
  {
87
- :data=>[
88
- {:attributes=>[[:name, "Ambühl"]], :id=>1, :image=>"src:customer-image",
89
- :associations=>{
90
- :articles=>[
91
- {:attributes=>[[:number, "01"]], :id=>1, :image=>"src:article-image"},
92
- {:attributes=>[[:number, "01"]], :id=>2, :image=>"src:article-image"}
93
- ]
94
- }
103
+ "data": [
104
+ {
105
+ "values": [
106
+ "cust"
107
+ ],
108
+ "attributes": {
109
+ "name": "cust"
110
+ },
111
+ "id": 1,
112
+ "model": "Customer",
113
+ "children": [
114
+ {
115
+ "values": [
116
+ "screw"
117
+ ],
118
+ "attributes": {
119
+ "name": "screw"
120
+ },
121
+ "id": 1,
122
+ "model": "Customer.Article",
123
+ "parent_id": 1,
124
+ }
125
+ ]
95
126
  }
96
- ],
97
- :length=>3,
98
- :overflow=>false,
99
- :columns_count=>1,
100
- :sub_records_columns_count=>1
127
+ ],
128
+ "length": 2,
129
+ "overflow": false,
130
+ "columns_count": 1,
131
+ "sub_records_columns_count": 0,
132
+ "image": false,
133
+ "time": 0.0075
134
+ }
135
+ ```
136
+
137
+ **query schema and result_schema**
138
+
139
+ The resulting hash for a record looks like this:
140
+
141
+ ```ruby
142
+ {
143
+ :values => ["abc", "123"],
144
+ :id => 456,
145
+ :model => "article",
146
+ :image => "src:img..."
147
+ }
148
+ ```
149
+
150
+ The resulting hash or json is built from the schema, which can look like this:
151
+
152
+ ```ruby
153
+ :number
154
+ ```
155
+
156
+ or
157
+
158
+ ```ruby
159
+ [:number, :name]
160
+ ```
161
+
162
+ or
163
+
164
+ ```ruby
165
+ {
166
+ values: [:number, :name],
167
+ image: :name_of_a_column_or_method
101
168
  }
102
169
  ```
103
170
 
171
+ There is a query schema and an output schema.
172
+ If no output schema is defined, the query schema is used for both.
173
+
174
+ ```ruby
175
+ Article.like('first', :name).generate_hash
176
+ # => :values => ["first"]
177
+ ```
178
+
179
+ If an output schema is specified, the result may differ from the search scope:
180
+
181
+ ```ruby
182
+ Article.like('first', :name).generate_hash(:number, :name)
183
+ # => :values => ["012", "first"]
184
+ ```
185
+
186
+ The collect class remembers the schema for a model:
187
+
188
+ ```ruby
189
+ c = LikeQuery::Collect.new
190
+ c.collect([:name]) { Article.like('x') }
191
+ c.collect { Article.like('x') } #=> schema [:name] is used
192
+ c.collect { Article.like('x', :number) } # => schema [:number] is used
193
+ ```
194
+
195
+ **#set_schema**
196
+
197
+ When a child returns its parent, the schema for the parent must be given.
198
+ Otherwise `#generate_hash` would not know what values to return.
199
+
200
+ ```ruby
201
+ c = LikeQuery::Collect.new
202
+ c.set_schema(Customer, :name)
203
+ # => now the customer will be returned with { values: [<name>] }
204
+ c.collect(parent: :customer) { Article.like('screw', :name) }
205
+ # => this will add the customer (unless it exists in the list) and add the Article as a child to the customer
206
+ r = c.generate_hash
207
+ ```
208
+
209
+ **Performance**
210
+
211
+ Values defined by the schema are processed by the `#send` method, but recursively.
212
+ This means, for example, that for an `article` with the given key `customer.employees.contact_details.email` in the schema would return the name of the associated customer.
213
+
214
+ ATTENTION: This can trigger a lot of database queries, depending on your structure or if or which method is behind the called names.
215
+
104
216
  ## Tests
105
217
 
106
- Tests for this gem, by rspec, are included not inside this gem, they can be found in a [test project](https://gitlab.com/sedl/like_query_project)
218
+ Tests for this gem, by rspec, are not included in this gem, they can be found in [test project](https://gitlab.com/sedl/like_query_project)
107
219
 
108
220
  - [ ] [Set up project integrations](https://gitlab.com/sedl/like_query/-/settings/integrations)
109
221
 
@@ -1,116 +1,86 @@
1
1
  module LikeQuery
2
2
  class Collect
3
- def initialize(limit = 50)
4
- @limit = limit
3
+ def initialize(limit = nil)
4
+ if limit
5
+ @limit = limit
6
+ elsif Rails.configuration.x.like_query.limit
7
+ @limit = Rails.configuration.x.like_query.limit
8
+ else
9
+ @limit = nil
10
+ end
5
11
  @length = 0
6
- @data = []
12
+ @data = {}
7
13
  @overflow = false
8
14
  @columns_count = 0
9
15
  @sub_records_columns_count = 0
10
16
  @image = false
11
- @sub_records_image = false
17
+ #@sub_records_image = false
18
+ @start_time = Time.now
19
+ @schemes = {}
20
+ @images = {}
21
+ end
22
+
23
+ def set_schema(model, schema)
24
+ @schemes[model.to_s] = schema_to_hash(schema)
12
25
  end
13
26
 
14
- def receive(*result_pattern, limit: nil, image: nil, &block)
27
+ def collect(output_schema = nil, limit: nil, parent: nil, image: nil, &block)
15
28
 
16
- return false if @length >= @limit
29
+ _limit = (limit ? (@limit && @limit < limit ? @limit : limit) : @limit)
30
+ return false if @length >= _limit
17
31
  length = 0
18
- @image = image.present?
19
32
 
20
- recs = yield
33
+ recs = yield.includes(parent).limit(_limit)
21
34
 
22
- _pattern = (result_pattern.present? ? result_pattern : recs.like_query_pattern)
23
- if _pattern.first.is_a?(Array)
24
- raise 'pattern can only be a array of hashes or symbols' if _pattern.length >= 2
25
- pattern = _pattern.first
26
- else
27
- pattern = _pattern
35
+ scm = (output_schema.present? ? output_schema : recs.like_query_schema)
36
+ if scm.present?
37
+ @schemes[recs.klass.to_s] = schema_to_hash(scm)
38
+ end
39
+ model_name = recs.klass.to_s
40
+ schema = @schemes[model_name] || schema_to_hash(nil)
41
+ _img = image || schema[:image]
42
+ if _img.present?
43
+ @images[model_name] = _img
44
+ @image = true
28
45
  end
29
46
 
30
- associations = []
31
- pattern.each do |p|
32
- if p.is_a?(Hash)
33
- associations += p.keys.map { |k| (k.to_sym >= :image ? nil : k.to_sym) }.compact
47
+ if parent
48
+ parent_assoc = recs.klass.reflect_on_association(parent)
49
+ if !parent_assoc
50
+ raise "parent «#{parent}» is not a valid association"
34
51
  end
35
52
  end
36
53
 
37
- recs.includes(associations).each do |r|
38
- rec_attr = {}
39
- pattern.each do |p|
40
- if p.is_a?(Hash)
54
+ recs.each do |rec|
41
55
 
42
- p.each do |assoc, cols|
43
-
44
- if assoc.to_sym == :image
45
-
46
- # raise 'Missing input: image column for given key :image' unless cols.present?
47
- #
48
- # # IMAGE COLUMN
49
- #
50
- # rec_attr[:image] = get_column_value(r, cols)
51
- # @image = true
52
-
53
- else
56
+ if @length >= _limit
57
+ @overflow = true
58
+ break
59
+ else
54
60
 
55
- # ASSOCIATIONS
56
-
57
- sub_records = r.send(assoc)
58
- image_column = nil
59
-
60
- _cols = if cols.is_a?(Hash)
61
- image_column = cols[:image]
62
- cols[:values]
63
- elsif cols.is_a?(Enumerable)
64
- cols
65
- else
66
- [cols]
67
- end
68
-
69
- (sub_records.is_a?(Enumerable) ? sub_records : [sub_records]).each do |sub_record|
70
- sub_attr = []
71
- _cols.each do |c|
72
- sub_attr.push(get_column_value(sub_record, c))
73
- end
74
-
75
- if @length >= @limit || (limit && length >= limit)
76
- @overflow = true
77
- break
78
- else
79
- c = sub_attr.length
80
- @sub_records_columns_count = c if c > @sub_records_columns_count
81
- rec_attr[:associations] ||= {}
82
- rec_attr[:associations][assoc] ||= []
83
- sub_hash = { values: sub_attr, id: sub_record.id, model: sub_record.class.to_s.underscore }
84
- if image_column
85
- sub_hash[:image] = get_column_value(sub_record, image_column)
86
- @sub_records_image = true
87
- end
88
- rec_attr[:associations][assoc].push(sub_hash)
89
- @length += 1
90
- length += 1
91
- end
92
- end
61
+ if parent
62
+ parent_record = rec.send(parent)
63
+ r = record_to_hash(rec, schema, image, parent_record)
64
+ parent_class_name = parent_record.class.to_s
65
+ parent_key = "#{parent_class_name}#{parent_record.id}"
66
+
67
+ unless @data[parent_key]
68
+ parent_schema = @schemes[parent_class_name]
69
+ unless parent_schema
70
+ Rails.logger.debug("WARNING: NO SCHEMA GIVEN FOR «#{parent_class_name}»")
71
+ parent_schema = schema_to_hash(nil)
93
72
  end
73
+ @data[parent_key] = record_to_hash(parent_record, parent_schema, @images[parent_class_name])
74
+ @length += 1
94
75
  end
95
- elsif p.is_a?(Symbol) || p.is_a?(String)
96
-
97
- # MAIN RECORD
98
-
99
- rec_attr[:values] ||= []
100
- rec_attr[:values].push(get_column_value(r, p))
101
- rec_attr[:id] = r.id
102
- rec_attr[:model] = r.class.to_s.underscore
103
- rec_attr[:image] = r.send(image) if @image
104
-
76
+ @data[parent_key][:children] ||= []
77
+ @data[parent_key][:children].push(r)
78
+ else
79
+ r = record_to_hash(rec, schema, image)
80
+ @data["#{rec.class}#{rec.id}"] = r
105
81
  end
106
- end
107
- if @length >= @limit || (limit && length >= limit)
108
- @overflow = true
109
- break
110
- else
111
- c = (image ? 1 : 0) + rec_attr[:values].length
82
+ c = (@image ? 1 : 0) + r[:values].to_a.length
112
83
  @columns_count = c if c > @columns_count
113
- @data.push(rec_attr)
114
84
  @length += 1
115
85
  length += 1
116
86
  end
@@ -119,23 +89,106 @@ module LikeQuery
119
89
  true
120
90
  end
121
91
 
122
- def result
92
+ def generate_hash
93
+ data = @data.map { |_, v| v }
123
94
  {
124
- data: @data,
95
+ data: data,
125
96
  length: @length,
126
97
  overflow: @overflow,
127
98
  columns_count: @columns_count,
128
99
  sub_records_columns_count: @sub_records_columns_count,
129
100
  image: @image,
130
- sub_records_image: @sub_records_image
101
+ # sub_records_image: @sub_records_image,
102
+ time: Time.now - @start_time
131
103
  }
132
104
  end
133
105
 
106
+ def generate_json
107
+ generate_hash.to_json
108
+ end
109
+
134
110
  private
135
111
 
112
+ def record_to_hash(record, schema, image, parent = nil)
113
+ r = {}
114
+ schema[:values].each do |k|
115
+ v = get_column_value(record, k)
116
+ r[:values] ||= []
117
+ r[:values].push(v)
118
+ r[:attributes] ||= {}
119
+ r[:attributes][k] = v
120
+ end
121
+ r[:id] = record.id
122
+ if parent
123
+ r[:model] = "#{parent.class.to_s}.#{record.class}"
124
+ r[:parent_id] = parent.id
125
+ else
126
+ r[:model] = record.class.to_s
127
+ end
128
+ r[:image] = record.send(image) if image
129
+ r
130
+ end
131
+
132
+ def schema_to_hash(schema)
133
+
134
+ if schema.is_a?(Array) && schema.first.is_a?(Array)
135
+ _schema = schema.first
136
+ raise 'invalid schema format' if schema.length >= 2
137
+ else
138
+ _schema = schema
139
+ end
140
+
141
+ if _schema.is_a?(String) || _schema.is_a?(Symbol)
142
+ { values: [_schema.to_sym] }
143
+ elsif _schema.is_a?(Array)
144
+ r = {}
145
+ _schema.each do |s|
146
+ if s.is_a?(String) || s.is_a?(Symbol)
147
+ r[:values] ||= []
148
+ r[:values].push(s.to_sym)
149
+ elsif s.is_a?(Hash)
150
+ s.each do |k, v|
151
+ if k.to_sym == :image
152
+ r[:image] = v
153
+ end
154
+ end
155
+ else
156
+ raise "invalid schema format (#{s}) in schema => «#{schema}»"
157
+ end
158
+ end
159
+ r
160
+ elsif _schema.is_a?(Hash)
161
+ _schema
162
+ elsif !schema.present?
163
+ { values: [] }
164
+ else
165
+ raise "invalid schema format => «#{schema}»"
166
+ end
167
+ end
168
+
136
169
  def get_column_value(record, column)
137
170
  val = nil
138
- column.to_s.split('.').each { |i| val = (val ? val : record).send(i) }
171
+ if column.is_a?(Hash)
172
+ r = []
173
+ column.each do |k, v|
174
+ if v.is_a?(Array)
175
+ v.each do |_v|
176
+ if _v.is_a?(String) || _v.is_a?(Symbol)
177
+ r.push(get_column_value(record, "#{k}.#{_v}"))
178
+ else
179
+ raise "Too deeply nested objects: #{v}"
180
+ end
181
+ end
182
+ elsif v.is_a?(String) || v.is_a?(Symbol)
183
+ r.push(get_column_value(record, "#{k}.#{v}"))
184
+ else
185
+ raise "query column value can only be done by string or symbol, but given: #{v}"
186
+ end
187
+ end
188
+ val = r.join(', ')
189
+ else
190
+ column.to_s.split('.').each { |i| val = (val ? val : record).send(i) }
191
+ end
139
192
  (val ? val : '')
140
193
  end
141
194
 
@@ -1,21 +1,21 @@
1
1
  module LikeQuery
2
2
  module ModelExtensions
3
3
 
4
- def like(search_string, *pattern)
4
+ def like(search_string, *schema)
5
5
 
6
6
  raise 'like can only be called from a model' if self == ApplicationRecord
7
7
 
8
8
  queries = nil
9
9
  associations = []
10
- @like_query_pattern = pattern
10
+ @like_query_schema = schema
11
11
 
12
12
  (search_string.is_a?(String) ? search_string.split(' ') : search_string).each do |s|
13
13
  str = "%#{s}%"
14
14
  q = nil
15
- if pattern.first.is_a?(Array) && pattern.length >= 2
16
- raise "only one array can be given: Either pattern as one array or as multiple args, not as array"
15
+ if schema.first.is_a?(Array) && schema.length >= 2
16
+ raise "only one array can be given: Either schema as one array or as multiple args, not as array"
17
17
  end
18
- (pattern.first.is_a?(Array) ? pattern.first : pattern).each do |p|
18
+ (schema.first.is_a?(Array) ? schema.first : schema).each do |p|
19
19
  if p.is_a?(Symbol) || p.is_a?(String)
20
20
  _q = arel_table[p].matches(str)
21
21
  q = (q ? q.or(_q) : _q)
@@ -42,28 +42,31 @@ module LikeQuery
42
42
  queries = (queries ? queries.and(q) : q)
43
43
  end
44
44
 
45
- @query = if associations.present?
46
- left_outer_joins(associations).where(
47
- queries
48
- )
49
- else
50
- where(
51
- queries
52
- )
53
- end
45
+ if associations.present?
46
+ left_outer_joins(associations).where(
47
+ queries
48
+ )
49
+ else
50
+ where(
51
+ queries
52
+ )
53
+ end
54
54
  end
55
55
 
56
- def like_result(*pattern, limit: 50, image: nil)
57
-
56
+ def generate_hash(output_schema = nil, limit: 50, image: nil)
57
+ c = LikeQuery::Collect.new(limit)
58
+ c.collect(output_schema, limit: limit, image: image) { all }
59
+ c.generate_hash
60
+ end
58
61
 
59
- raise 'has to be called behind #like' unless @like_query_pattern.is_a?(Array)
62
+ def generate_json(output_schema = nil, limit: 50, image: nil)
60
63
  c = LikeQuery::Collect.new(limit)
61
- c.receive(*pattern, limit: limit, image: image){@query}
62
- c.result
64
+ c.collect(output_schema, limit: limit, image: image) { all }
65
+ c.generate_json
63
66
  end
64
67
 
65
- def like_query_pattern
66
- @like_query_pattern
68
+ def like_query_schema
69
+ @like_query_schema
67
70
  end
68
71
 
69
72
  end
@@ -1,3 +1,3 @@
1
1
  module LikeQuery
2
- VERSION = "0.0.6"
2
+ VERSION = "0.0.8"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: like_query
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.6
4
+ version: 0.0.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - christian
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-08-28 00:00:00.000000000 Z
11
+ date: 2023-09-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails