like_query 0.0.6 → 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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