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