atomic_json 0.1.0

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.
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'atomic_json/json_query_helpers'
4
+
5
+ module AtomicJson
6
+ class QueryBuilder
7
+
8
+ include JsonQueryHelpers
9
+
10
+ attr_reader :record, :connection
11
+
12
+ delegate :quote_column_name, :quote_table_name, :quote, to: :connection
13
+
14
+ def initialize(record, connection)
15
+ @record = record
16
+ @connection = connection
17
+ end
18
+
19
+ def build(attributes, touch)
20
+ <<~SQL
21
+ UPDATE #{quote_table_name(record.class.table_name)}
22
+ SET #{build_set_subquery(attributes, touch)}
23
+ WHERE id = #{quote(record.id)};
24
+ SQL
25
+ end
26
+
27
+ private
28
+
29
+ def build_set_subquery(attributes, touch)
30
+ updates = json_updates_agg(attributes)
31
+ updates << timestamp_update_string if touch && record.has_attribute?(:updated_at)
32
+ updates.join(',')
33
+ end
34
+
35
+ def json_updates_agg(attributes)
36
+ attributes.map do |column, payload|
37
+ "#{quote_column_name(column)} = #{json_deep_merge(column, payload)}"
38
+ end
39
+ end
40
+
41
+ def timestamp_update_string
42
+ "#{quote_column_name(:updated_at)} = #{quote(Time.now)}"
43
+ end
44
+
45
+ def json_deep_merge(target, payload)
46
+ loop do
47
+ keys, value = traverse_payload(Hash[*payload.shift])
48
+ target = jsonb_set_query_string(target, keys, value)
49
+ break target if payload.empty?
50
+ end
51
+ end
52
+
53
+ ##
54
+ # Traverse the Hash payload, incrementally
55
+ # aggregating all hash keys into an array
56
+ # and use the last child as value
57
+ def traverse_payload(key_value_pair, keys = [])
58
+ loop do
59
+ key, val = key_value_pair.flatten
60
+ keys << key.to_s
61
+ break [keys, val] unless single_value_hash?(val)
62
+ key_value_pair = val
63
+ end
64
+ end
65
+
66
+ def jsonb_set_query_string(target, keys, value)
67
+ <<~EOF
68
+ jsonb_set(
69
+ #{target}::jsonb,
70
+ #{jsonb_quote_keys(keys)},
71
+ #{multi_value_hash?(value) ? concatenation(target, keys, value) : jsonb_quote_value(value)}
72
+ )::jsonb
73
+ EOF
74
+ end
75
+
76
+ def multi_value_hash?(value)
77
+ value.is_a?(Hash) && value.keys.count > 1
78
+ end
79
+
80
+ def single_value_hash?(value)
81
+ value.is_a?(Hash) && value.keys.count == 1
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'atomic_json/query'
4
+
5
+ module AtomicJson
6
+ module QueryMethods
7
+
8
+ extend ActiveSupport::Concern
9
+
10
+ def json_update(input)
11
+ run_callbacks(:save) do
12
+ Query.new(self)
13
+ .build(input, touch: true)
14
+ .execute!
15
+ reload.validate
16
+ end
17
+ end
18
+
19
+ def json_update!(input)
20
+ run_callbacks(:save) do
21
+ Query.new(self)
22
+ .build(input, touch: true)
23
+ .execute!
24
+ reload.validate!
25
+ end
26
+ end
27
+
28
+ def json_update_columns(input)
29
+ Query.new(self)
30
+ .build(input)
31
+ .execute!
32
+ end
33
+
34
+ end
35
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'atomic_json/errors'
4
+
5
+ module AtomicJson
6
+ module Validations
7
+
8
+ def validate_record!(record)
9
+ raise ActiveRecordError, 'cannot update a new record' if record.new_record?
10
+ raise ActiveRecordError, 'cannot update a destroyed record' if record.destroyed?
11
+ end
12
+
13
+ def validate_attributes!(record, attributes)
14
+ raise TypeError, 'Payload to update must be a hash' unless attributes.is_a?(Hash)
15
+ attributes.each_key do |key|
16
+ raise ReadOnlyAttributeError, "#{key} is marked as readonly" if read_only_attribute?(record, key)
17
+ raise InvalidColumnTypeError, 'ActiveRecord column needs to be of type JSON or JSONB' unless valid_column_type?(record, key)
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def read_only_attribute?(record, key)
24
+ record.class.readonly_attributes.include?(key.to_s)
25
+ end
26
+
27
+ def valid_column_type?(record, key)
28
+ %i[json jsonb].include?(record.type_for_attribute(key.to_s).type)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,5 @@
1
+ module AtomicJson
2
+
3
+ VERSION = '0.1.0'.freeze
4
+
5
+ end
metadata ADDED
@@ -0,0 +1,225 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: atomic_json
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Antoine Macia
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2018-08-14 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '5.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '5.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activesupport
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '5.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '5.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: pg
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.18'
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: 0.18.1
51
+ type: :runtime
52
+ prerelease: false
53
+ version_requirements: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - "~>"
56
+ - !ruby/object:Gem::Version
57
+ version: '0.18'
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: 0.18.1
61
+ - !ruby/object:Gem::Dependency
62
+ name: bundler
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '1.16'
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '1.16'
75
+ - !ruby/object:Gem::Dependency
76
+ name: byebug
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '10.0'
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ version: 10.0.2
85
+ type: :development
86
+ prerelease: false
87
+ version_requirements: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - "~>"
90
+ - !ruby/object:Gem::Version
91
+ version: '10.0'
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ version: 10.0.2
95
+ - !ruby/object:Gem::Dependency
96
+ name: factory_bot
97
+ requirement: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - "~>"
100
+ - !ruby/object:Gem::Version
101
+ version: '4.0'
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - "~>"
107
+ - !ruby/object:Gem::Version
108
+ version: '4.0'
109
+ - !ruby/object:Gem::Dependency
110
+ name: minitest
111
+ requirement: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - "~>"
114
+ - !ruby/object:Gem::Version
115
+ version: '5.0'
116
+ type: :development
117
+ prerelease: false
118
+ version_requirements: !ruby/object:Gem::Requirement
119
+ requirements:
120
+ - - "~>"
121
+ - !ruby/object:Gem::Version
122
+ version: '5.0'
123
+ - !ruby/object:Gem::Dependency
124
+ name: rake
125
+ requirement: !ruby/object:Gem::Requirement
126
+ requirements:
127
+ - - "~>"
128
+ - !ruby/object:Gem::Version
129
+ version: '10.0'
130
+ type: :development
131
+ prerelease: false
132
+ version_requirements: !ruby/object:Gem::Requirement
133
+ requirements:
134
+ - - "~>"
135
+ - !ruby/object:Gem::Version
136
+ version: '10.0'
137
+ - !ruby/object:Gem::Dependency
138
+ name: rubocop
139
+ requirement: !ruby/object:Gem::Requirement
140
+ requirements:
141
+ - - "~>"
142
+ - !ruby/object:Gem::Version
143
+ version: 0.58.1
144
+ type: :development
145
+ prerelease: false
146
+ version_requirements: !ruby/object:Gem::Requirement
147
+ requirements:
148
+ - - "~>"
149
+ - !ruby/object:Gem::Version
150
+ version: 0.58.1
151
+ - !ruby/object:Gem::Dependency
152
+ name: standalone_migrations
153
+ requirement: !ruby/object:Gem::Requirement
154
+ requirements:
155
+ - - "~>"
156
+ - !ruby/object:Gem::Version
157
+ version: '5.2'
158
+ - - ">="
159
+ - !ruby/object:Gem::Version
160
+ version: 5.2.5
161
+ type: :development
162
+ prerelease: false
163
+ version_requirements: !ruby/object:Gem::Requirement
164
+ requirements:
165
+ - - "~>"
166
+ - !ruby/object:Gem::Version
167
+ version: '5.2'
168
+ - - ">="
169
+ - !ruby/object:Gem::Version
170
+ version: 5.2.5
171
+ description: ''
172
+ email:
173
+ - antoine@discolabs.com
174
+ executables: []
175
+ extensions: []
176
+ extra_rdoc_files: []
177
+ files:
178
+ - ".gitignore"
179
+ - ".rubocop.yml"
180
+ - ".travis.yml"
181
+ - Gemfile
182
+ - Gemfile.lock
183
+ - LICENSE.txt
184
+ - README.md
185
+ - Rakefile
186
+ - VERSION
187
+ - atomic_json.gemspec
188
+ - bin/console
189
+ - bin/setup
190
+ - db/config.yml
191
+ - db/migrate/20180715062407_create_mock_test_table.rb
192
+ - db/schema.rb
193
+ - lib/atomic_json.rb
194
+ - lib/atomic_json/errors.rb
195
+ - lib/atomic_json/json_query_helpers.rb
196
+ - lib/atomic_json/query.rb
197
+ - lib/atomic_json/query_builder.rb
198
+ - lib/atomic_json/query_methods.rb
199
+ - lib/atomic_json/validations.rb
200
+ - lib/atomic_json/version.rb
201
+ homepage: ''
202
+ licenses:
203
+ - MIT
204
+ metadata: {}
205
+ post_install_message:
206
+ rdoc_options: []
207
+ require_paths:
208
+ - lib
209
+ required_ruby_version: !ruby/object:Gem::Requirement
210
+ requirements:
211
+ - - ">="
212
+ - !ruby/object:Gem::Version
213
+ version: '0'
214
+ required_rubygems_version: !ruby/object:Gem::Requirement
215
+ requirements:
216
+ - - ">="
217
+ - !ruby/object:Gem::Version
218
+ version: '0'
219
+ requirements: []
220
+ rubyforge_project:
221
+ rubygems_version: 2.5.2
222
+ signing_key:
223
+ specification_version: 4
224
+ summary: Atomic update of JSON/JSONB fields for ActiveRecord models
225
+ test_files: []