post_json 1.0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +194 -0
- data/Rakefile +21 -0
- data/lib/core_ext/abstract_adapter_extend.rb +16 -0
- data/lib/core_ext/active_record_relation_extend.rb +16 -0
- data/lib/core_ext/hash_extend.rb +36 -0
- data/lib/generators/post_json/install/install_generator.rb +32 -0
- data/lib/generators/post_json/install/templates/create_post_json_documents.rb +13 -0
- data/lib/generators/post_json/install/templates/create_post_json_dynamic_indexes.rb +9 -0
- data/lib/generators/post_json/install/templates/create_post_json_model_settings.rb +18 -0
- data/lib/generators/post_json/install/templates/create_procedures.rb +120 -0
- data/lib/generators/post_json/install/templates/enable_extensions.rb +28 -0
- data/lib/generators/post_json/install/templates/initializer.rb +9 -0
- data/lib/post_json.rb +56 -0
- data/lib/post_json/base.rb +278 -0
- data/lib/post_json/concerns/argument_methods.rb +33 -0
- data/lib/post_json/concerns/dynamic_index_methods.rb +34 -0
- data/lib/post_json/concerns/finder_methods.rb +343 -0
- data/lib/post_json/concerns/query_methods.rb +157 -0
- data/lib/post_json/concerns/settings_methods.rb +106 -0
- data/lib/post_json/dynamic_index.rb +99 -0
- data/lib/post_json/model_settings.rb +17 -0
- data/lib/post_json/query_translator.rb +48 -0
- data/lib/post_json/version.rb +3 -0
- data/spec/dummy/Rakefile +6 -0
- data/spec/dummy/app/assets/javascripts/application.js +13 -0
- data/spec/dummy/app/assets/stylesheets/application.css +13 -0
- data/spec/dummy/app/assets/stylesheets/scaffold.css +56 -0
- data/spec/dummy/app/controllers/application_controller.rb +5 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/views/layouts/application.html.erb +14 -0
- data/spec/dummy/bin/bundle +3 -0
- data/spec/dummy/bin/rails +4 -0
- data/spec/dummy/bin/rake +4 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/config/application.rb +30 -0
- data/spec/dummy/config/boot.rb +5 -0
- data/spec/dummy/config/database.yml +26 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +29 -0
- data/spec/dummy/config/environments/production.rb +80 -0
- data/spec/dummy/config/environments/test.rb +36 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/spec/dummy/config/initializers/inflections.rb +16 -0
- data/spec/dummy/config/initializers/mime_types.rb +5 -0
- data/spec/dummy/config/initializers/post_json.rb +5 -0
- data/spec/dummy/config/initializers/secret_token.rb +12 -0
- data/spec/dummy/config/initializers/session_store.rb +3 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +23 -0
- data/spec/dummy/config/routes.rb +56 -0
- data/spec/dummy/db/migrate/20131015022029_enable_extensions.rb +28 -0
- data/spec/dummy/db/migrate/20131015022030_create_procedures.rb +120 -0
- data/spec/dummy/db/migrate/20131015022031_create_post_json_model_settings.rb +18 -0
- data/spec/dummy/db/migrate/20131015022032_create_post_json_collections.rb +16 -0
- data/spec/dummy/db/migrate/20131015022033_create_post_json_documents.rb +13 -0
- data/spec/dummy/db/migrate/20131015022034_create_post_json_dynamic_indexes.rb +9 -0
- data/spec/dummy/db/structure.sql +311 -0
- data/spec/dummy/public/404.html +58 -0
- data/spec/dummy/public/422.html +58 -0
- data/spec/dummy/public/500.html +57 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/models/base_spec.rb +393 -0
- data/spec/models/collection_spec.rb +27 -0
- data/spec/models/queries_spec.rb +164 -0
- data/spec/modules/argument_methods_spec.rb +17 -0
- data/spec/modules/query_methods_spec.rb +69 -0
- data/spec/spec_helper.rb +54 -0
- 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
|
data/spec/dummy/Rakefile
ADDED
@@ -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 .
|