post_json 1.0.3

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.
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