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.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/.rubocop.yml +683 -0
- data/.travis.yml +5 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +111 -0
- data/LICENSE.txt +21 -0
- data/README.md +83 -0
- data/Rakefile +17 -0
- data/VERSION +1 -0
- data/atomic_json.gemspec +35 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/db/config.yml +8 -0
- data/db/migrate/20180715062407_create_mock_test_table.rb +11 -0
- data/db/schema.rb +25 -0
- data/lib/atomic_json.rb +9 -0
- data/lib/atomic_json/errors.rb +28 -0
- data/lib/atomic_json/json_query_helpers.rb +19 -0
- data/lib/atomic_json/query.rb +40 -0
- data/lib/atomic_json/query_builder.rb +84 -0
- data/lib/atomic_json/query_methods.rb +35 -0
- data/lib/atomic_json/validations.rb +31 -0
- data/lib/atomic_json/version.rb +5 -0
- metadata +225 -0
@@ -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
|
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: []
|