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,157 @@
1
+ module PostJson
2
+ module QueryMethods
3
+ extend ActiveSupport::Concern
4
+
5
+ #
6
+ # Use 'query_tree' to integrate with this module
7
+ #
8
+
9
+ include ArgumentMethods
10
+
11
+ def query_tree
12
+ @query_tree ||= {}
13
+ end
14
+
15
+ def query_tree_renew!
16
+ @query_tree = @query_tree.deep_dup
17
+ end
18
+
19
+ def query_clone
20
+ cloned_self = clone
21
+ cloned_self.query_tree_renew!
22
+ cloned_self
23
+ end
24
+
25
+ def add_query(method, *arguments)
26
+ query_tree[method] = (query_tree[method] || []) + arguments
27
+ self
28
+ end
29
+
30
+ def except!(*attributes)
31
+ remove_keys = flatten_arguments(attributes).map(&:to_sym)
32
+ query_tree.except!(*remove_keys)
33
+ self
34
+ end
35
+
36
+ def except(*attributes)
37
+ query_clone.except!(*attributes)
38
+ end
39
+
40
+ def limit!(value)
41
+ except!(:limit).add_query(:limit, value.to_i)
42
+ end
43
+
44
+ def limit(value)
45
+ query_clone.limit!(value)
46
+ end
47
+
48
+ def offset!(value)
49
+ except!(:offset).add_query(:offset, value.to_i)
50
+ end
51
+
52
+ def offset(value)
53
+ query_clone.offset!(value)
54
+ end
55
+
56
+ def page!(page, per_page)
57
+ page_int = page.to_i
58
+ per_page_int = per_page.to_i
59
+ offset!((page_int-1)*per_page_int).limit!(per_page_int)
60
+ end
61
+
62
+ def page(page, per_page)
63
+ query_clone.page!(page, per_page)
64
+ end
65
+
66
+ def only!(*attributes)
67
+ keep_keys = flatten_arguments(attributes).map(&:to_sym)
68
+ query_tree.keep_if{|key| key.in?(keep_keys)}
69
+ self
70
+ end
71
+
72
+ def only(*attributes)
73
+ query_clone.only!(*attributes)
74
+ end
75
+
76
+ def order!(*args)
77
+ if 0 < args.length
78
+ flatten_arguments(args).each do |arg|
79
+ name, direction = arg.split(' ')
80
+
81
+ direction = direction.to_s.upcase
82
+ direction = "ASC" unless direction.present?
83
+
84
+
85
+ if direction.in?(["ASC", "DESC"]) == false
86
+ raise ArgumentError, "Direction should be 'asc' or 'desc'"
87
+ end
88
+
89
+ add_query(:order, "#{name} #{direction}")
90
+ end
91
+ end
92
+ self
93
+ end
94
+
95
+ def order(*args)
96
+ query_clone.order!(*args)
97
+ end
98
+
99
+ def reorder!(*args)
100
+ except!(:order).order!(*args)
101
+ end
102
+
103
+ def reorder(*args)
104
+ query_clone.reorder!(*args)
105
+ end
106
+
107
+ def reverse_order!
108
+ current_order = query_tree.delete(:order)
109
+ if current_order.present?
110
+ current_order.each do |arg|
111
+ name, direction = arg.split(' ')
112
+ reverse_direction = direction == "DESC" ? "ASC" : "DESC"
113
+ order!("#{name} #{reverse_direction}")
114
+ end
115
+ self
116
+ else
117
+ order!("id DESC")
118
+ end
119
+ end
120
+
121
+ alias_method :reverse!, :reverse_order!
122
+
123
+ def reverse_order
124
+ query_clone.reverse_order!
125
+ end
126
+
127
+ alias_method :reverse, :reverse_order
128
+
129
+ def where!(opts = :chain, *rest)
130
+ if opts == :chain || opts.blank?
131
+ self
132
+ else
133
+ case opts
134
+ when String
135
+ if opts.start_with?("function")
136
+ add_query(:where_function, {function: opts, arguments: rest})
137
+ else
138
+ add_query(:where_forward, [opts] + rest)
139
+ end
140
+ when Array
141
+ add_query(:where_forward, [opts] + rest)
142
+ when Hash
143
+ opts.stringify_keys.flatten_hash.each do |attribute, value|
144
+ add_query(:where_equal, {attribute: attribute, argument: value})
145
+ end
146
+ else
147
+ add_query(:where_forward, [opts] + rest)
148
+ end
149
+ self
150
+ end
151
+ end
152
+
153
+ def where(filter = :none, *options)
154
+ query_clone.where!(filter, *options)
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,106 @@
1
+ module PostJson
2
+ module SettingsMethods
3
+ extend ActiveSupport::Concern
4
+
5
+ def __model__settings
6
+ @__model__settings ||= self.class.find_settings_or_create
7
+ end
8
+
9
+ module ClassMethods
10
+ def find_settings
11
+ ModelSettings.by_collection(collection_name).first
12
+ end
13
+
14
+ def find_settings_or_create
15
+ ModelSettings.by_collection(collection_name).first_or_create(collection_name: collection_name)
16
+ end
17
+
18
+ def find_settings_or_initialize
19
+ ModelSettings.by_collection(collection_name).first_or_initialize(collection_name: collection_name)
20
+ end
21
+
22
+ def read_settings_attribute(attribute_name)
23
+ attribute_name = attribute_name.to_s
24
+ settings = find_settings_or_initialize
25
+ settings[attribute_name]
26
+ end
27
+
28
+ def write_settings_attribute(attribute_name, value)
29
+ attribute_name = attribute_name.to_s
30
+ settings = find_settings_or_initialize
31
+ if settings[attribute_name] != value
32
+ settings[attribute_name] = value
33
+ settings.save!
34
+ end
35
+ value
36
+ end
37
+
38
+ def meta
39
+ HashWithIndifferentAccess.new(read_settings_attribute('meta'))
40
+ end
41
+
42
+ def meta=(hash)
43
+ write_settings_attribute('meta', HashWithIndifferentAccess.new(hash))
44
+ end
45
+
46
+ def use_timestamps
47
+ read_settings_attribute('use_timestamps')
48
+ end
49
+
50
+ def use_timestamps=(value)
51
+ write_settings_attribute('use_timestamps', value)
52
+ end
53
+
54
+ alias_method :record_timestamps, :use_timestamps
55
+ alias_method :record_timestamps=, :use_timestamps=
56
+
57
+ def created_at_attribute_name
58
+ read_settings_attribute('created_at_attribute_name')
59
+ end
60
+
61
+ def created_at_attribute_name=(attribute_name)
62
+ write_settings_attribute('created_at_attribute_name', attribute_name)
63
+ end
64
+
65
+ def updated_at_attribute_name
66
+ read_settings_attribute('updated_at_attribute_name')
67
+ end
68
+
69
+ def updated_at_attribute_name=(attribute_name)
70
+ write_settings_attribute('updated_at_attribute_name', attribute_name)
71
+ end
72
+
73
+ def include_version_number
74
+ read_settings_attribute('include_version_number')
75
+ end
76
+
77
+ def include_version_number=(value)
78
+ write_settings_attribute('include_version_number', value)
79
+ end
80
+
81
+ def version_attribute_name
82
+ read_settings_attribute('version_attribute_name')
83
+ end
84
+
85
+ def version_attribute_name=(attribute_name)
86
+ write_settings_attribute('version_attribute_name', attribute_name)
87
+ end
88
+
89
+ def use_dynamic_index
90
+ read_settings_attribute('use_dynamic_index')
91
+ end
92
+
93
+ def use_dynamic_index=(value)
94
+ write_settings_attribute('use_dynamic_index', value)
95
+ end
96
+
97
+ def create_dynamic_index_milliseconds_threshold
98
+ read_settings_attribute('create_dynamic_index_milliseconds_threshold')
99
+ end
100
+
101
+ def create_dynamic_index_milliseconds_threshold=(millisecs)
102
+ write_settings_attribute('create_dynamic_index_milliseconds_threshold', millisecs)
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,99 @@
1
+ module PostJson
2
+ class DynamicIndex < ActiveRecord::Base
3
+ class << self
4
+ include ArgumentMethods
5
+
6
+ def ensure_index(model_settings_id, *selectors)
7
+ selectors = flatten_arguments(selectors)
8
+ if selectors.length == 0
9
+ []
10
+ else
11
+ existing_selectors = where(model_settings_id: model_settings_id).pluck(:selector)
12
+ new_selectors = selectors - existing_selectors
13
+ new_selectors.map do |selector|
14
+ create(model_settings_id: model_settings_id, selector: selector)
15
+ end
16
+ end
17
+ end
18
+
19
+ def indexed_selectors(model_settings_id)
20
+ # distinct is needed since race condition can cause 1+ records to own the same index
21
+ where(model_settings_id: model_settings_id).distinct.pluck(:selector)
22
+ end
23
+
24
+ def destroy_index(model_settings_id, selector)
25
+ where(model_settings_id: model_settings_id, selector: selector).destroy_all.present?
26
+ end
27
+ end
28
+
29
+ self.table_name = "post_json_dynamic_indexes"
30
+
31
+ belongs_to :model_settings
32
+
33
+ attr_readonly :selector
34
+
35
+ validates :selector, presence: true
36
+
37
+ def index_name
38
+ # if defined?(@index_name)
39
+ # @index_name
40
+ # else
41
+ # prefix = "dyn_#{model_settings_id.gsub('-', '')}_"
42
+ # @index_name = if 63 < prefix.length + selector.length
43
+ # digest = Digest::MD5.hexdigest(selector)
44
+ # "#{prefix}#{digest}"[0..62]
45
+ # else
46
+ # "#{prefix}#{selector.gsub('.', '_')}"
47
+ # end
48
+ # end
49
+
50
+ @index_name ||= unless @index_name
51
+ prefix = "dyn_#{model_settings_id.gsub('-', '')}_"
52
+ if 63 < prefix.length + selector.length
53
+ digest = Digest::MD5.hexdigest(selector)
54
+ "#{prefix}#{digest}"[0..62]
55
+ else
56
+ "#{prefix}#{selector.gsub('.', '_')}"
57
+ end
58
+ end
59
+ end
60
+
61
+ after_create do |dynamic_index|
62
+ begin
63
+ ActiveRecord::Base.connection.execute(dynamic_index.inline_create_index_procedure)
64
+ rescue ActiveRecord::StatementInvalid => e
65
+ # lets ignore this exception if the index already exists. this could happen in a rare race condition.
66
+ orig = e.original_exception
67
+ raise unless orig.is_a?(PG::DuplicateTable) && orig.message.include?("already exists")
68
+ end
69
+ end
70
+
71
+ after_destroy do |dynamic_index|
72
+ ActiveRecord::Base.connection.execute("DROP INDEX IF EXISTS #{dynamic_index.index_name};")
73
+ end
74
+
75
+ def inline_create_index_procedure
76
+ schemas = ActiveRecord::Base.connection.schema_search_path.gsub(/\s+/, '').split(',')
77
+ current_schema = if schemas[0] == "\"$user\"" && 1 < schemas.length
78
+ schemas[1]
79
+ else
80
+ schemas[0]
81
+ end
82
+ "DO $$
83
+ BEGIN
84
+
85
+ IF NOT EXISTS (
86
+ SELECT 1
87
+ FROM pg_class c
88
+ JOIN pg_namespace n ON n.oid = c.relnamespace
89
+ WHERE c.relname = '#{index_name}'
90
+ AND n.nspname = '#{current_schema}' -- 'public' by default
91
+ ) THEN
92
+
93
+ CREATE INDEX #{index_name} ON #{current_schema}.#{Base.table_name} (json_selector('#{selector}', __doc__body)) WHERE __doc__model_settings_id = '#{model_settings_id.gsub('-', '')}';
94
+ END IF;
95
+
96
+ END$$;"
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,17 @@
1
+ module PostJson
2
+ class ModelSettings < ActiveRecord::Base
3
+ self.table_name = "post_json_model_settings"
4
+
5
+ before_validation do |settings|
6
+ settings.collection_name = settings.collection_name.to_s.strip
7
+ end
8
+
9
+ scope :by_collection, ->(name) { where("lower(collection_name) = ?", name.to_s.strip.downcase) }
10
+
11
+ class << self
12
+ def collection_name_digest(name)
13
+ Digest::MD5.hexdigest(name.to_s.strip.downcase)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,48 @@
1
+ module PostJson
2
+ class QueryTranslator
3
+
4
+ include FinderMethods
5
+
6
+ def initialize(relation)
7
+ @relation = relation
8
+ end
9
+
10
+ def model_class
11
+ @relation.klass
12
+ end
13
+
14
+ def table_name
15
+ model_class.table_name
16
+ end
17
+
18
+ def each(&block)
19
+ relation_query.each(&block)
20
+ end
21
+
22
+ def execute(ignore_dynamic_indexes = false, &block)
23
+ if ignore_dynamic_indexes == true || model_class.use_dynamic_index != true
24
+ block.call(relation_query)
25
+ else
26
+ result = block.call(relation_query)
27
+ select_query = ActiveRecord::Base.connection.last_select_query
28
+ select_duration = ActiveRecord::Base.connection.last_select_query_duration * 1000
29
+ if model_class.use_dynamic_index == true &&
30
+ model_class.create_dynamic_index_milliseconds_threshold < select_duration
31
+ selectors = select_query.scan(/.*?json_selector\('(.*?)', __doc__body\)/).flatten.uniq
32
+ model_class.create_dynamic_indexes(selectors)
33
+ end
34
+ result
35
+ end
36
+ end
37
+
38
+ def relation_query
39
+ active_record_send_invocations.inject(@relation) do |query, send_arguments|
40
+ query.send(*send_arguments)
41
+ end
42
+ end
43
+
44
+ def create(attributes = {})
45
+ relation_query.create(attributes.with_indifferent_access)
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,3 @@
1
+ module PostJson
2
+ VERSION = "1.0.3"
3
+ end
@@ -0,0 +1,6 @@
1
+ # Add your own tasks in files placed in lib/tasks ending in .rake,
2
+ # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3
+
4
+ require File.expand_path('../config/application', __FILE__)
5
+
6
+ Dummy::Application.load_tasks
@@ -0,0 +1,13 @@
1
+ // This is a manifest file that'll be compiled into application.js, which will include all the files
2
+ // listed below.
3
+ //
4
+ // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
5
+ // or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path.
6
+ //
7
+ // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8
+ // compiled file.
9
+ //
10
+ // Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details
11
+ // about supported directives.
12
+ //
13
+ //= require_tree .