post_json 1.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +194 -0
  4. data/Rakefile +21 -0
  5. data/lib/core_ext/abstract_adapter_extend.rb +16 -0
  6. data/lib/core_ext/active_record_relation_extend.rb +16 -0
  7. data/lib/core_ext/hash_extend.rb +36 -0
  8. data/lib/generators/post_json/install/install_generator.rb +32 -0
  9. data/lib/generators/post_json/install/templates/create_post_json_documents.rb +13 -0
  10. data/lib/generators/post_json/install/templates/create_post_json_dynamic_indexes.rb +9 -0
  11. data/lib/generators/post_json/install/templates/create_post_json_model_settings.rb +18 -0
  12. data/lib/generators/post_json/install/templates/create_procedures.rb +120 -0
  13. data/lib/generators/post_json/install/templates/enable_extensions.rb +28 -0
  14. data/lib/generators/post_json/install/templates/initializer.rb +9 -0
  15. data/lib/post_json.rb +56 -0
  16. data/lib/post_json/base.rb +278 -0
  17. data/lib/post_json/concerns/argument_methods.rb +33 -0
  18. data/lib/post_json/concerns/dynamic_index_methods.rb +34 -0
  19. data/lib/post_json/concerns/finder_methods.rb +343 -0
  20. data/lib/post_json/concerns/query_methods.rb +157 -0
  21. data/lib/post_json/concerns/settings_methods.rb +106 -0
  22. data/lib/post_json/dynamic_index.rb +99 -0
  23. data/lib/post_json/model_settings.rb +17 -0
  24. data/lib/post_json/query_translator.rb +48 -0
  25. data/lib/post_json/version.rb +3 -0
  26. data/spec/dummy/Rakefile +6 -0
  27. data/spec/dummy/app/assets/javascripts/application.js +13 -0
  28. data/spec/dummy/app/assets/stylesheets/application.css +13 -0
  29. data/spec/dummy/app/assets/stylesheets/scaffold.css +56 -0
  30. data/spec/dummy/app/controllers/application_controller.rb +5 -0
  31. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  32. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  33. data/spec/dummy/bin/bundle +3 -0
  34. data/spec/dummy/bin/rails +4 -0
  35. data/spec/dummy/bin/rake +4 -0
  36. data/spec/dummy/config.ru +4 -0
  37. data/spec/dummy/config/application.rb +30 -0
  38. data/spec/dummy/config/boot.rb +5 -0
  39. data/spec/dummy/config/database.yml +26 -0
  40. data/spec/dummy/config/environment.rb +5 -0
  41. data/spec/dummy/config/environments/development.rb +29 -0
  42. data/spec/dummy/config/environments/production.rb +80 -0
  43. data/spec/dummy/config/environments/test.rb +36 -0
  44. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  45. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  46. data/spec/dummy/config/initializers/inflections.rb +16 -0
  47. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  48. data/spec/dummy/config/initializers/post_json.rb +5 -0
  49. data/spec/dummy/config/initializers/secret_token.rb +12 -0
  50. data/spec/dummy/config/initializers/session_store.rb +3 -0
  51. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  52. data/spec/dummy/config/locales/en.yml +23 -0
  53. data/spec/dummy/config/routes.rb +56 -0
  54. data/spec/dummy/db/migrate/20131015022029_enable_extensions.rb +28 -0
  55. data/spec/dummy/db/migrate/20131015022030_create_procedures.rb +120 -0
  56. data/spec/dummy/db/migrate/20131015022031_create_post_json_model_settings.rb +18 -0
  57. data/spec/dummy/db/migrate/20131015022032_create_post_json_collections.rb +16 -0
  58. data/spec/dummy/db/migrate/20131015022033_create_post_json_documents.rb +13 -0
  59. data/spec/dummy/db/migrate/20131015022034_create_post_json_dynamic_indexes.rb +9 -0
  60. data/spec/dummy/db/structure.sql +311 -0
  61. data/spec/dummy/public/404.html +58 -0
  62. data/spec/dummy/public/422.html +58 -0
  63. data/spec/dummy/public/500.html +57 -0
  64. data/spec/dummy/public/favicon.ico +0 -0
  65. data/spec/models/base_spec.rb +393 -0
  66. data/spec/models/collection_spec.rb +27 -0
  67. data/spec/models/queries_spec.rb +164 -0
  68. data/spec/modules/argument_methods_spec.rb +17 -0
  69. data/spec/modules/query_methods_spec.rb +69 -0
  70. data/spec/spec_helper.rb +54 -0
  71. metadata +184 -0
@@ -0,0 +1,28 @@
1
+ class EnableExtensions < ActiveRecord::Migration
2
+ def change
3
+ # Install Postgresql 9.2 with PLV8 (might require )
4
+ # 1. sudo add-apt-repository ppa:pitti/postgresql
5
+ # 2. sudo apt-get update
6
+ # 3. sudo apt-get install postgresql-9.2 postgresql-contrib-9.2 postgresql-server-dev-9.2 libv8-dev
7
+ # 4. sudo su - postgres
8
+ # 5. Enter psql and run "CREATE USER webnuts WITH PASSWORD 'webnuts';" and "ALTER USER webnuts CREATEDB;" and "ALTER USER webnuts SUPERUSER;"
9
+ # 6. git clone https://code.google.com/p/plv8js/
10
+ # 7. cd plv8js
11
+ # 8. make
12
+ # 9. sudo make install
13
+
14
+ # bundle exec rake db:create:all
15
+ # bundle exec rake db:migrate
16
+ # bundle exec rake db:test:prepare
17
+
18
+ # See all available extensions:
19
+ # ActiveRecord::Base.connection.execute("select * from pg_available_extensions;").each_row do |row|
20
+ # puts row.to_s
21
+ # end
22
+
23
+ enable_extension 'uuid-ossp' # generate universally unique identifiers (UUIDs)
24
+ enable_extension 'hstore' # data type for storing sets of (key, value) pairs
25
+ enable_extension 'plv8' # PL/JavaScript (v8) trusted procedural language
26
+ #enable_extension 'plcoffee' # PL/CoffeeScript (v8) trusted procedural language
27
+ end
28
+ end
@@ -0,0 +1,9 @@
1
+ # PostJson.setup "people" do |collection|
2
+ # collection.record_timestamps = true # default is 'true'
3
+ # collection.created_at_attribute_name = "created_at" # default is 'created_at'
4
+ # collection.updated_at_attribute_name = "updated_at" # default is 'updated_at'
5
+ # collection.include_version_number = true # default is 'true'
6
+ # collection.version_attribute_name = "version" # default is 'version'
7
+ # collection.use_dynamic_index = true # default is 'true'
8
+ # collection.create_dynamic_index_milliseconds_threshold = 50 # default is '50'
9
+ # end
data/lib/post_json.rb ADDED
@@ -0,0 +1,56 @@
1
+ require 'core_ext/hash_extend'
2
+ require 'core_ext/abstract_adapter_extend'
3
+ require 'core_ext/active_record_relation_extend'
4
+ require 'post_json/concerns/argument_methods'
5
+ require 'post_json/concerns/query_methods'
6
+ require 'post_json/concerns/finder_methods'
7
+ require 'post_json/concerns/settings_methods'
8
+ require 'post_json/concerns/dynamic_index_methods'
9
+ require 'post_json/query_translator'
10
+ require 'post_json/base'
11
+ require 'post_json/model_settings'
12
+ require 'post_json/dynamic_index'
13
+ require 'post_json/version'
14
+
15
+ module PostJson
16
+ class << self
17
+ def setup(collection_name, &block)
18
+ collection = Collection[collection_name]
19
+ collection.transaction do
20
+ block.call(collection)
21
+ end
22
+ end
23
+ end
24
+
25
+ class Collection
26
+ module Proxy
27
+ class << self
28
+ def const_missing(class_name)
29
+ const_set(class_name, Class.new(PostJson::Base))
30
+ end
31
+ end
32
+ end
33
+
34
+ class << self
35
+ def [](collection_name)
36
+ name_digest = PostJson::ModelSettings.collection_name_digest(collection_name)
37
+ class_name = "Collection_#{name_digest}"
38
+ model_class = Proxy.const_get(class_name)
39
+ model_class.collection_name = collection_name
40
+ model_class
41
+ end
42
+
43
+ def names
44
+ ModelSettings.order('collection_name').pluck('collection_name')
45
+ end
46
+
47
+ def to_a
48
+ names.map { |collection_name| self[collection_name] }
49
+ end
50
+
51
+ def each(&block)
52
+ to_a.each(&block)
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,278 @@
1
+ module PostJson
2
+ class Base < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ self.table_name = "post_json_documents"
5
+ self.lock_optimistically = false
6
+
7
+ include SettingsMethods
8
+ include DynamicIndexMethods
9
+
10
+ def initialize(*args)
11
+ __local__primary_key = self.class.primary_key
12
+ __local__attrs = (args[0] || {}).with_indifferent_access
13
+ __local__attrs[__local__primary_key] = __local__attrs[__local__primary_key].to_s if __local__attrs.has_key?(__local__primary_key)
14
+ args[0] = {__local__primary_key => __local__attrs[__local__primary_key], '__doc__body' => __local__attrs}.with_indifferent_access
15
+ super
16
+ end
17
+
18
+ def cache_key
19
+ @dashed_name ||= self.class.name.underscore.dasherize
20
+ __local__unique_version = __doc__version || Digest::MD5.hexdigest(attributes.inspect)
21
+ "#{@dashed_name}-#{id}-version-#{__local__unique_version}"
22
+ end
23
+
24
+ def attributes
25
+ read_attribute('__doc__body').try(:with_indifferent_access) || HashWithIndifferentAccess.new
26
+ end
27
+
28
+ def to_h
29
+ attributes.deep_dup
30
+ end
31
+
32
+ def write_attribute(attribute_name, value)
33
+ attribute_name = attribute_name.to_s
34
+ if attribute_name == '__doc__body'
35
+ value = value.try(:with_indifferent_access)
36
+ self.__doc__body_will_change! unless self.__doc__body.try(:with_indifferent_access) == value
37
+ super('__doc__body', value)
38
+ elsif attribute_name.in?(attribute_names)
39
+ super
40
+ else
41
+ __doc__body_write_attribute(attribute_name, value)
42
+ end
43
+ end
44
+
45
+ def attribute_changed?(attribute_name)
46
+ attribute_name = attribute_name.to_s
47
+ if attribute_name.in?(attribute_names)
48
+ super
49
+ else
50
+ __doc__body_attribute_changed?(attribute_name)
51
+ end
52
+ end
53
+
54
+ def __doc__body_read_attribute(attribute_name)
55
+ __local__value = self.__doc__body[attribute_name.to_s] if self.__doc__body
56
+ __doc__body_convert_attribute_type(attribute_name, __local__value)
57
+ end
58
+
59
+ def __doc__body_write_attribute(attribute_name, value)
60
+ self.__doc__body = HashWithIndifferentAccess.new(self.__doc__body).merge(attribute_name.to_s => value)
61
+ value
62
+ end
63
+
64
+ def __doc__body_attribute_was(attribute_name)
65
+ self.__doc__body_was == nil ? nil : self.__doc__body_was.with_indifferent_access[attribute_name]
66
+ end
67
+
68
+ def __doc__body_attribute_changed?(attribute_name)
69
+ (self.__doc__body == nil ? nil : self.__doc__body.with_indifferent_access[attribute_name]) != self.__doc__body_attribute_was(attribute_name)
70
+ end
71
+
72
+ def __doc__body_attribute_change(attribute_name)
73
+ __local__change = [__doc__body_attribute_was(attribute_name), __doc__body_read_attribute(attribute_name)]
74
+ if __local__change[0] == __local__change[1]
75
+ nil
76
+ else
77
+ __local__change
78
+ end
79
+ end
80
+
81
+ def __doc__body_convert_attribute_type(attribute_name, value)
82
+ case value
83
+ when /^[0-9]{4}-[0-1][0-9]-[0-3][0-9]T[0-2][0-9]:[0-5][0-9]:[0-5][0-9]\.[0-9]{3}Z$/
84
+ Time.parse(value).in_time_zone
85
+ when Hash
86
+ value.inject(HashWithIndifferentAccess.new) do |result, (key, value)|
87
+ result[key] = convert_document_attribute_type("#{attribute_name}.#{key}", value)
88
+ result
89
+ end
90
+ when Array
91
+ value.map.with_index do |array_value, index|
92
+ convert_document_attribute_type("#{attribute_name}[#{index}]", array_value)
93
+ end
94
+ else
95
+ value
96
+ end
97
+ end
98
+
99
+ def [](attribute_name)
100
+ self.__doc__body_read_attribute(attribute_name)
101
+ end
102
+
103
+ def []=(attribute_name, value)
104
+ self.__doc__body_write_attribute(attribute_name, value)
105
+ end
106
+
107
+ def respond_to?(method_symbol, include_all = false)
108
+ if super
109
+ true
110
+ else
111
+ method_name = method_symbol.to_s
112
+ attribute_name = if method_name.end_with?("_changed?")
113
+ method_name[0..-10]
114
+ elsif method_name.end_with?("_was")
115
+ method_name[0..-5]
116
+ elsif method_name.end_with?("=")
117
+ method_name[0..-2]
118
+ elsif method_name.end_with?("_change")
119
+ method_name[0..-8]
120
+ else
121
+ method_name
122
+ end
123
+ attributes.has_key?(attribute_name)
124
+ end
125
+ end
126
+
127
+ def method_missing(method_symbol, *args, &block)
128
+ method_name = method_symbol.to_s
129
+ attribute_name = if method_name.end_with?("_changed?")
130
+ method_name[0..-10]
131
+ elsif method_name.end_with?("_was")
132
+ method_name[0..-5]
133
+ elsif method_name.end_with?("=")
134
+ method_name[0..-2]
135
+ elsif method_name.end_with?("_change")
136
+ method_name[0..-8]
137
+ else
138
+ method_name
139
+ end
140
+
141
+ if attribute_name.in?(attribute_names) == false
142
+ self.class.define_attribute_accessor(attribute_name)
143
+ send(method_symbol, *args)
144
+ else
145
+ super
146
+ end
147
+ end
148
+
149
+ class << self
150
+ def post_json_all
151
+ QueryTranslator.new(original_all)
152
+ end
153
+
154
+ alias_method :original_all, :all
155
+ alias_method :all, :post_json_all
156
+
157
+ def page(*args)
158
+ all.page(*args)
159
+ end
160
+
161
+ def default_scopes
162
+ if @collection_name
163
+ model_settings = ModelSettings.table_name
164
+ # query = original_all.joins("INNER JOIN #{model_settings} ON #{table_name}.__doc__model_settings_id = #{model_settings}.id")
165
+ # query = query.where("lower(#{model_settings}.collection_name) = ?", collection_name.downcase)
166
+
167
+ query = original_all.joins("INNER JOIN #{model_settings} ON lower(#{model_settings}.collection_name) = '#{collection_name.downcase}'")
168
+ query = query.where("#{table_name}.__doc__model_settings_id = #{model_settings}.id")
169
+
170
+ [Proc.new { query }] + super
171
+ else
172
+ [Proc.new { none }] + super
173
+ end
174
+ end
175
+
176
+ def collection_name
177
+ message = "You need to assign a collection name to class \"#{name}\":
178
+ class #{name}
179
+ self.collection_name = \"people\"
180
+ end"
181
+ raise ArgumentError, message unless @collection_name.present?
182
+ @collection_name
183
+ end
184
+
185
+ def collection_name=(name)
186
+ raise ArgumentError, "Collection name must be present" unless name.present?
187
+ @collection_name = name.to_s.strip
188
+ end
189
+
190
+ def rename_collection(new_name)
191
+ new_name = new_name.to_s.strip
192
+ settings = find_settings
193
+ if settings
194
+ settings.collection_name = new_name
195
+ settings.save!
196
+ end
197
+ @collection_name = new_name
198
+ end
199
+
200
+ def define_attribute_accessor(attribute_name)
201
+ class_eval <<-RUBY
202
+ def #{attribute_name}
203
+ __doc__body_read_attribute('#{attribute_name}')
204
+ end
205
+
206
+ def #{attribute_name}=(value)
207
+ __doc__body_write_attribute('#{attribute_name}', value)
208
+ end
209
+
210
+ def #{attribute_name}_changed?
211
+ __doc__body_attribute_changed?('#{attribute_name}')
212
+ end
213
+
214
+ def #{attribute_name}_was
215
+ __doc__body_attribute_was('#{attribute_name}')
216
+ end
217
+
218
+ def #{attribute_name}_change
219
+ __doc__body_attribute_change('#{attribute_name}')
220
+ end
221
+ RUBY
222
+ end
223
+ end
224
+
225
+ protected
226
+
227
+ def timestamp_attributes_for_update
228
+ [] # ActiveRecord depend on real table columns, so we use an alternative timestamps method
229
+ end
230
+
231
+ def timestamp_attributes_for_create
232
+ [] # ActiveRecord depend on real table columns, so we use an alternative timestamps method
233
+ end
234
+
235
+ def create_record
236
+ self.id = self.__doc__body['id'].to_s.strip.downcase
237
+ if self.id.blank?
238
+ self.id = self.__doc__body['id'] = SecureRandom.uuid
239
+ end
240
+
241
+ self.__doc__model_settings_id = __model__settings.id
242
+ self.__doc__version = 1
243
+
244
+ if __model__settings.include_version_number == true &&
245
+ __doc__body_read_attribute(__model__settings.version_attribute_name) == nil
246
+ __doc__body_write_attribute(__model__settings.version_attribute_name, self.__doc__version)
247
+ end
248
+
249
+ if __model__settings.use_timestamps
250
+ __local__current_time = Time.zone.now.strftime('%Y-%m-%dT%H:%M:%S.%LZ')
251
+ __doc__body_write_attribute(__model__settings.created_at_attribute_name, __local__current_time)
252
+ __doc__body_write_attribute(__model__settings.updated_at_attribute_name, __local__current_time)
253
+ end
254
+ super
255
+ end
256
+
257
+ def update_record(*args)
258
+ if self.changed_attributes.keys.include?(self.class.primary_key)
259
+ raise ArgumentError, "Primary key '#{self.class.primary_key}' cannot be modified."
260
+ end
261
+
262
+ if self.__doc__body_changed?
263
+ self.__doc__version = self.__doc__version + 1
264
+ end
265
+
266
+ if __model__settings.include_version_number == true &&
267
+ __doc__body_attribute_changed?(__model__settings.version_attribute_name) == false
268
+ __doc__body_write_attribute(__model__settings.version_attribute_name, self.__doc__version)
269
+ end
270
+
271
+ if __model__settings.use_timestamps && __doc__body_attribute_changed?(__model__settings.updated_at_attribute_name)
272
+ __local__current_time = Time.zone.now.strftime('%Y-%m-%dT%H:%M:%S.%LZ')
273
+ __doc__body_write_attribute(__model__settings.updated_at_attribute_name, __local__current_time)
274
+ end
275
+ super
276
+ end
277
+ end
278
+ end
@@ -0,0 +1,33 @@
1
+ module PostJson
2
+ module ArgumentMethods
3
+ extend ActiveSupport::Concern
4
+
5
+ def join_arguments(*arguments)
6
+ arguments = arguments[0] if arguments.length == 1 && arguments[0].is_a?(Array)
7
+ arguments = arguments[0].split(',') if arguments.length == 1 && arguments[0].is_a?(String)
8
+
9
+ arguments = arguments.map do |arg|
10
+ case arg
11
+ when nil
12
+ nil
13
+ when String
14
+ arg.strip.gsub(/\s+/, ' ')
15
+ when Symbol
16
+ arg.to_s
17
+ else
18
+ arg
19
+ end
20
+ end
21
+
22
+ arguments.join(',')
23
+ end
24
+
25
+ def flatten_arguments(*arguments)
26
+ join_arguments(*arguments).split(',')
27
+ end
28
+
29
+ def assert_valid_indifferent_keys(options, *valid_keys)
30
+ options.stringify_keys.assert_valid_keys(flatten_arguments(*valid_keys))
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,34 @@
1
+ module PostJson
2
+ module DynamicIndexMethods
3
+ extend ActiveSupport::Concern
4
+
5
+ module ClassMethods
6
+ def dynamic_indexes
7
+ settings = find_settings
8
+ if settings
9
+ DynamicIndex.indexed_selectors(settings.id)
10
+ else
11
+ []
12
+ end
13
+ end
14
+
15
+ def create_dynamic_index(selector)
16
+ create_dynamic_indexes(selector)
17
+ end
18
+
19
+ def create_dynamic_indexes(*selectors)
20
+ settings = find_settings_or_create
21
+ DynamicIndex.ensure_index(settings.id, *selectors).count
22
+ end
23
+
24
+ def destroy_dynamic_index(selector)
25
+ settings = find_settings_or_create
26
+ if settings
27
+ DynamicIndex.destroy_index(settings.id, selector)
28
+ else
29
+ false
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,343 @@
1
+ require "hashie"
2
+
3
+ module PostJson
4
+ module FinderMethods
5
+ extend ActiveSupport::Concern
6
+
7
+ #
8
+ # This module depends upon method 'execute'
9
+ #
10
+
11
+ include QueryMethods
12
+ include ArgumentMethods
13
+
14
+ def any?
15
+ execute { |documents| documents.any? }
16
+ end
17
+
18
+ def blank?
19
+ execute { |documents| documents.blank? }
20
+ end
21
+
22
+ def count(column_name = nil, options = {})
23
+ selector = if column_name.present?
24
+ define_selector(column_name)
25
+ else
26
+ column_name
27
+ end
28
+ execute { |documents| documents.count(selector, options) }
29
+ end
30
+
31
+ def delete(id_or_array)
32
+ where(id: id_or_array).delete_all
33
+ end
34
+
35
+ def delete_all(conditions = nil)
36
+ where(conditions).execute { |documents| documents.delete_all }
37
+ end
38
+
39
+ def destroy(id)
40
+ execute { |documents| documents.destroy(id) }
41
+ end
42
+
43
+ def destroy_all(conditions = nil)
44
+ where(conditions).execute { |documents| documents.destroy_all }
45
+ end
46
+
47
+ def empty?
48
+ execute { |documents| documents.empty? }
49
+ end
50
+
51
+ def exists?(conditions = nil)
52
+ query = case conditions
53
+ when nil
54
+ self
55
+ when Numeric, String
56
+ where({id: conditions})
57
+ else
58
+ where(conditions)
59
+ end
60
+
61
+ query.execute { |documents| documents.exists? }
62
+ end
63
+
64
+ def find(*args)
65
+ execute { |documents| documents.find(*args) }
66
+ end
67
+
68
+ def find_by(*args)
69
+ where(*args).first
70
+ end
71
+
72
+ def find_by!(*args)
73
+ find_by(*args) or raise ActiveRecord::RecordNotFound
74
+ end
75
+
76
+ def find_each(options = {})
77
+ execute { |documents| documents.find_each(options) }
78
+ end
79
+
80
+ def find_in_batches(options = {})
81
+ execute { |documents| documents.find_in_batches(options) }
82
+ end
83
+
84
+ def first(limit = nil)
85
+ if limit
86
+ limit(limit).execute { |documents| documents.first }
87
+ else
88
+ execute { |documents| documents.first }
89
+ end
90
+ end
91
+
92
+ def first!
93
+ execute { |documents| documents.first! }
94
+ end
95
+
96
+ def first_or_create(attributes = {})
97
+ attributes = where_values_hash.with_indifferent_access.deep_merge(attributes)
98
+ first or create(attributes)
99
+ end
100
+
101
+ def first_or_initialize(attributes = {})
102
+ attributes = where_values_hash.with_indifferent_access.deep_merge(attributes)
103
+ first or model_class.new(attributes)
104
+ end
105
+
106
+ def ids
107
+ pluck('id')
108
+ end
109
+
110
+ def last(limit = nil)
111
+ reverse_order.first(limit)
112
+ end
113
+
114
+ def last!
115
+ reverse_order.first!
116
+ end
117
+
118
+ def load
119
+ execute { |documents| documents.load }
120
+ end
121
+
122
+ def many?
123
+ execute { |documents| documents.many? }
124
+ end
125
+
126
+ def pluck(*selectors)
127
+ selectors = join_arguments(*selectors)
128
+ if selectors == ""
129
+ []
130
+ elsif selectors == "*"
131
+ execute { |documents| documents.pluck("\"#{table_name}\".__doc__body") }
132
+ elsif selectors == "id"
133
+ execute { |documents| documents.pluck("\"#{table_name}\".id") }
134
+ else
135
+ result = nil
136
+ execute { |documents| result = documents.pluck("json_selectors('#{selectors}', \"#{table_name}\".__doc__body)") }
137
+ if selectors.include?(",")
138
+ result
139
+ else
140
+ result.flatten(1)
141
+ end
142
+ end
143
+ end
144
+
145
+ def select(*selectors)
146
+ selectors = selectors.flatten(1)
147
+ if selectors.length == 0
148
+ []
149
+ elsif selectors.length == 1
150
+ selector = selectors[0]
151
+ case selector
152
+ when String, Symbol
153
+ selector = selector.to_s.gsub(/\s+/, '')
154
+ if selector == "*"
155
+ pluck("*").map { |body| body ? Hashie::Mash.new(body) : body }
156
+ else
157
+ selectors = selector.split(",")
158
+ if selectors.length == 1
159
+ select({selector => selector})
160
+ else
161
+ select(selectors)
162
+ end
163
+ end
164
+ when Hash
165
+ flat_hash = selector.flatten_hash
166
+ pluck(flat_hash.values).map do |row|
167
+ flat_body = if flat_hash.keys.length == 1
168
+ {flat_hash.keys[0] => row}
169
+ else
170
+ Hash[flat_hash.keys.zip(row)]
171
+ end
172
+ deep_hash = flat_body.deepen_hash
173
+ Hashie::Mash.new(deep_hash) if deep_hash
174
+ end
175
+ else
176
+ raise ArgumentError, "Invalid argument(s): #{selectors.inspect}"
177
+ end
178
+ else
179
+ select(Hash[selectors.zip(selectors)])
180
+ end
181
+ end
182
+
183
+ def size
184
+ execute { |documents| documents.size }
185
+ end
186
+
187
+ def take(limit = nil)
188
+ execute { |documents| documents.take(limit) }
189
+ end
190
+
191
+ def take!
192
+ execute { |documents| documents.take! }
193
+ end
194
+
195
+ def to_a
196
+ execute { |documents| documents.to_a }
197
+ end
198
+
199
+ def to_sql
200
+ execute { |documents| documents.to_sql }
201
+ end
202
+
203
+ def where_values_hash
204
+ where_equals = query_tree[:where_equal] || []
205
+ values_hash = where_equals.inject({}) do |result, where_equal|
206
+ key = where_equal[:attribute]
207
+ result[key] = where_equal[:argument]
208
+ result
209
+ end
210
+ values_hash.deepen_hash
211
+ end
212
+
213
+ protected
214
+
215
+ def define_selector(attribute_name)
216
+ case attribute_name.to_s
217
+ when "id"
218
+ "\"#{table_name}\".id"
219
+ else
220
+ "json_selector('#{attribute_name}', \"#{table_name}\".__doc__body)"
221
+ end
222
+ end
223
+
224
+ def prepare_query_tree_for_method_mapping(query_tree)
225
+ prepared_query_tree = query_tree.map do |method_sym, arguments_collection|
226
+ arguments_collection.map do |arguments|
227
+ case method_sym
228
+ when :limit
229
+ [:limit, arguments]
230
+ when :offset
231
+ [:offset, arguments]
232
+ when :order
233
+ name, direction = arguments.split(" ")
234
+ selector = define_selector(name)
235
+ [:order, "#{selector} #{direction}"]
236
+ when :where_function
237
+ function = arguments[:function]
238
+ escape_sql_single_quote = function.gsub("'", "''")
239
+ condition = "js_filter('#{escape_sql_single_quote}', '#{arguments[:arguments]}', \"#{table_name}\".__doc__body) = 1"
240
+ [:where, condition]
241
+ when :where_forward
242
+ if arguments[0].is_a?(String)
243
+ json_regex = "json_([^ =]+)"
244
+ arguments[0] = arguments[0].gsub(/^#{json_regex}\ /) {"json_selector('#{$1}', \"#{table_name}\".__doc__body) "}
245
+ arguments[0] = arguments[0].gsub(/\ #{json_regex}\ /) {" json_selector('#{$1}', \"#{table_name}\".__doc__body) "}
246
+ arguments[0] = arguments[0].gsub(/\ #{json_regex}$/) {" json_selector('#{$1}', \"#{table_name}\".__doc__body)"}
247
+ end
248
+ [:where, arguments]
249
+ when :where_equal
250
+ selector = define_selector(arguments[:attribute])
251
+ argument = arguments[:argument]
252
+ case argument
253
+ when Array
254
+ values = argument.map{|v| v ? v.to_s : nil}
255
+ [:where, "(#{selector} IN (?))", values]
256
+ when Range
257
+ first_value = argument.first ? argument.first.to_s : nil
258
+ last_value = argument.last ? argument.last.to_s : nil
259
+ [:where, "(#{selector} BETWEEN ? AND ?)", first_value, last_value]
260
+ else
261
+ value = argument ? argument.to_s : nil
262
+ [:where, "#{selector} = ?", value]
263
+ end
264
+ else
265
+ raise NotImplementedError, "Query tree method '#{method_sym}' not mapped to Active Record."
266
+ end
267
+ end
268
+ end
269
+ prepared_query_tree.flatten(1)
270
+ end
271
+
272
+ def active_record_send_invocations
273
+ prepare_query_tree_for_method_mapping(query_tree)
274
+ end
275
+ end
276
+ end
277
+
278
+ # def query(params)
279
+ # valid_params = params.slice(:filter, :filter_arguments, :count, :page, :per_page, :order, :limit, :offset, :select)
280
+
281
+ # inquiry = self
282
+ # inquiry = inquiry.where(valid_params[:filter], valid_params[:filter_arguments]) if valid_params[:filter].present?
283
+
284
+ # total_count = inquiry.count
285
+
286
+ # if valid_params[:count].to_s.downcase.in? ["true", "yes", "1"]
287
+ # total_count
288
+ # else
289
+ # meta = {
290
+ # page: 1,
291
+ # per_page: nil,
292
+ # total_pages: 1,
293
+ # total_count: total_count,
294
+ # }
295
+
296
+ # page, per_page = valid_params[:page].to_i, valid_params[:per_page].to_i
297
+
298
+ # inquiry = if valid_params[:page].present?
299
+ # total_pages = total_count/per_page
300
+ # total_pages = total_pages + 1 if 0 < total_count%per_page
301
+ # meta[:page] = page
302
+ # meta[:per_page] = per_page
303
+ # meta[:total_pages] = total_pages
304
+ # inquiry.page(page, per_page)
305
+ # else
306
+ # inquiry
307
+ # end
308
+
309
+ # inquiry = inquiry.order(valid_params[:order]) if valid_params[:order].present?
310
+ # inquiry = inquiry.limit(valid_params[:limit].to_i) if valid_params[:limit].present?
311
+ # inquiry = inquiry.offset(valid_params[:offset].to_i) if valid_params[:offset].present?
312
+
313
+ # meta[:updated_at] = inquiry.except(:order)
314
+ # .order('updatedAt desc')
315
+ # .limit(1)
316
+ # .select("updatedAt")
317
+ # .first
318
+ # .try("updatedAt")
319
+
320
+ # bodies = if valid_params[:select].present?
321
+ # inquiry.select(valid_params[:select])
322
+ # else
323
+ # inquiry.pluck("body")
324
+ # end
325
+
326
+ # {documents: bodies, meta: meta}
327
+ # end
328
+ # end
329
+
330
+ # def digest_etag(seed = nil)
331
+ # inquery = except(:order).order('id')
332
+ # digest_seed = (inquery.pluck(:etag) + [inquery.to_sql + seed.to_s]).join
333
+ # etag = Digest::SHA2.hexdigest(digest_seed)
334
+ # end
335
+
336
+ # def latest_updated_at
337
+ # except(:order).order('updated_at DESC').limit(1).pluck(:updated_at).first
338
+ # end
339
+
340
+ # def xor_uuids(uuid_array)
341
+ # # move work to db: http://stackoverflow.com/questions/17739887/how-to-xor-md5-hash-values-and-cast-them-to-hex-in-postgresql
342
+ # uuid_array.inject("") { |result, uuid| (result.to_i(16) ^ uuid.to_i(16)).to_s(16) }
343
+ # end