revisable 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 4a689bd4a025ca4964e233c27e82610dac766a357638cd238881a8aa3df70ab9
4
+ data.tar.gz: e699b99e853085603d3bf24a3afb2c70fa86202adc91b5ec5457aecd28f95ebe
5
+ SHA512:
6
+ metadata.gz: b99e4b430666ee6f134c2faea2954ea3470572a82142dcc51c438abf19215d8883a1eb6391948deeaa559df747d37bcadd5020fa05679ac3f46a70f98aa42305
7
+ data.tar.gz: 7acc9f61e70b786d074626b1911dd4925cd603f3217ae5a16d6881a057d1b01cb3e066bf9bc98c893e9193de2140331b0f520ee26c6cf6e545d44f1697eadfc6
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Brad Gessler
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,309 @@
1
+ # Revisable
2
+
3
+ Git-like versioning for ActiveRecord text content. Branches, merges, diffs, tags, and publishing — in your database.
4
+
5
+ ## Why not PaperTrail / Audited / Logidze?
6
+
7
+ Every existing Rails versioning gem gives you a **linear audit log**. That's fine for tracking "who changed what when," but it falls apart when you need real content workflows:
8
+
9
+ | | PaperTrail | Audited | Logidze | **Revisable** |
10
+ |---|---|---|---|---|
11
+ | Branching | No | No | No | **Yes** |
12
+ | Merging | No | No | No | **Yes** |
13
+ | Diffing | Adjacent only | No | Between versions | **Any two refs** |
14
+ | Tags / named versions | No | No | No | **Yes** |
15
+ | Content deduplication | No | No | No | **SHA-based** |
16
+ | Publish workflow | No | No | No | **Tag → publish** |
17
+ | Storage model | Full snapshots | After-state | Incremental JSON | **Content-addressed blobs** |
18
+
19
+ PaperTrail and friends answer "what happened?" Revisable answers "what's the state of this content, on this branch, and how does it differ from that branch?"
20
+
21
+ ### When to use Revisable
22
+
23
+ You're building a CMS, docs site, legal document system, or any app where **content goes through drafts, review, and publishing**. Multiple people might edit the same article. You want to see diffs, branch off drafts, and tag releases.
24
+
25
+ ### When to use PaperTrail
26
+
27
+ You want an audit log for compliance. You need to know that `user_42` changed `price` from `19.99` to `24.99` at 3:47pm. Linear history is fine. You don't need branches or diffs.
28
+
29
+ ## Installation
30
+
31
+ Add to your Gemfile:
32
+
33
+ ```ruby
34
+ gem "revisable"
35
+ ```
36
+
37
+ Generate the migration:
38
+
39
+ ```bash
40
+ rails generate revisable:install
41
+ rails db:migrate
42
+ ```
43
+
44
+ ## Setup
45
+
46
+ ```ruby
47
+ class Post < ApplicationRecord
48
+ include Revisable::Model
49
+ revisable :title, :body
50
+ end
51
+ ```
52
+
53
+ That's it. Any text/string columns you pass to `revisable` get git-like version control.
54
+
55
+ ## ActiveRecord Integration
56
+
57
+ ### Auto-commit on save
58
+
59
+ Revisable hooks into `after_create` and `after_update`. When you save a model with revisable fields, a commit is created automatically:
60
+
61
+ ```ruby
62
+ post = Post.create!(title: "First Draft", body: "Hello world")
63
+ # => auto-creates initial commit on "main"
64
+
65
+ post.update!(title: "Better Title")
66
+ # => auto-creates commit "Updated title" on "main"
67
+
68
+ post.repository.log("main").map(&:message)
69
+ # => ["Updated title", "Initial version"]
70
+ ```
71
+
72
+ To disable auto-commit (for models where you want manual control):
73
+
74
+ ```ruby
75
+ class LegalDocument < ApplicationRecord
76
+ include Revisable::Model
77
+ revisable :content, :summary, auto_commit: false
78
+ end
79
+ ```
80
+
81
+ ### Author tracking
82
+
83
+ Set the current author in your controller so auto-commits know who made the change:
84
+
85
+ ```ruby
86
+ class ApplicationController < ActionController::Base
87
+ include Revisable::ActiveRecord::ControllerHelpers
88
+
89
+ private
90
+
91
+ def revisable_author
92
+ current_user
93
+ end
94
+ end
95
+ ```
96
+
97
+ Now every save within a request automatically records the author:
98
+
99
+ ```ruby
100
+ post.update!(title: "New Title")
101
+ post.repository.log("main").first.author # => #<User id: 1, name: "Alice">
102
+ ```
103
+
104
+ You can also set the author explicitly for background jobs or console work:
105
+
106
+ ```ruby
107
+ Revisable::CurrentAuthor.with(admin_user) do
108
+ post.update!(body: "Bulk-edited content")
109
+ end
110
+ ```
111
+
112
+ ## Usage
113
+
114
+ ### Manual commits
115
+
116
+ For more control, use the repository directly. Pass only the fields that changed — unchanged fields carry forward automatically.
117
+
118
+ ```ruby
119
+ repo = post.repository
120
+
121
+ repo.commit!(
122
+ message: "First draft",
123
+ author: current_user,
124
+ fields: { title: "How to Deploy Rails", body: "# Step 1\n..." }
125
+ )
126
+
127
+ # Later, update just the body
128
+ repo.commit!(
129
+ message: "Added step 2",
130
+ author: current_user,
131
+ fields: { body: "# Step 1\n...\n# Step 2\n..." }
132
+ )
133
+ ```
134
+
135
+ Content is stored as SHA-256 addressed blobs. If you revert a title to a previous value, no new storage is used — it points to the existing blob.
136
+
137
+ ### Reading
138
+
139
+ ```ruby
140
+ # Read current state of any branch or tag
141
+ snapshot = repo.at("main")
142
+ snapshot.title # => "How to Deploy Rails"
143
+ snapshot.body # => "# Step 1\n..."
144
+ snapshot.to_h # => { title: "...", body: "..." }
145
+
146
+ # Read by tag or SHA
147
+ repo.at("v2").title
148
+ repo.at("abc123").title
149
+ ```
150
+
151
+ ### Branching
152
+
153
+ ```ruby
154
+ repo.branch!("amy-draft", from: "main")
155
+
156
+ repo.commit!(
157
+ branch: "amy-draft",
158
+ message: "Rewrote intro",
159
+ author: amy,
160
+ fields: { body: "# Better intro\n..." }
161
+ )
162
+
163
+ # main is untouched
164
+ repo.at("main").body # => original
165
+ repo.at("amy-draft").body # => rewritten
166
+ ```
167
+
168
+ ### Diffing
169
+
170
+ Compare any two refs, commits, or tags. Diffs are field-level — blob SHAs are compared first, so unchanged fields skip text diffing entirely.
171
+
172
+ ```ruby
173
+ diff = repo.diff("main", "amy-draft")
174
+
175
+ diff.changed? # => true
176
+ diff.changed_fields # => [:body]
177
+
178
+ diff.field(:title).status # => :unchanged
179
+ diff.field(:body).status # => :modified
180
+
181
+ # Unified text diff (like `git diff`)
182
+ puts diff.field(:body).to_text
183
+ # --- a/body
184
+ # +++ b/body
185
+ # -# Step 1
186
+ # +# Better intro
187
+
188
+ # HTML diff (for web UIs)
189
+ diff.field(:body).to_html
190
+ # <div class="revisable-diff" data-field="body">
191
+ # <del># Step 1</del>
192
+ # <ins># Better intro</ins>
193
+ # </div>
194
+
195
+ # Combined diff across all fields
196
+ diff.to_text
197
+ diff.to_html
198
+ ```
199
+
200
+ ### Merging
201
+
202
+ Three-way merge using the common ancestor. Fields that only changed on one side auto-resolve. Fields changed on both sides are conflicts.
203
+
204
+ ```ruby
205
+ merge = repo.merge("amy-draft", into: "main")
206
+
207
+ merge.clean? # => false
208
+ merge.conflicts # => [:body]
209
+ merge.auto_resolved # => [:title]
210
+
211
+ # Inspect a conflict — each field is a MergeField object
212
+ field = merge.field(:body)
213
+ field.status # => :conflicted
214
+ field.ours # => main's version
215
+ field.theirs # => amy-draft's version
216
+ field.ancestor # => common ancestor
217
+
218
+ # Resolve it (on the field directly, or via the merge)
219
+ field.resolve("hand-written resolution")
220
+ field.resolve(:ours) # pick main's version
221
+ field.resolve(:theirs) # pick amy's version
222
+
223
+ # Or resolve all conflicts at once
224
+ merge.each do |name, f|
225
+ f.resolve(:theirs) if f.conflicted?
226
+ end
227
+
228
+ # Commit the merge (creates a two-parent commit)
229
+ merge.commit!(repository: repo, author: current_user, message: "Merged Amy's draft")
230
+ ```
231
+
232
+ ### Tags and publishing
233
+
234
+ Tags are immutable pointers to a commit. Publishing materializes a tag's content into the model's actual columns — so your views and APIs read plain ActiveRecord attributes with zero versioning overhead.
235
+
236
+ ```ruby
237
+ # Tag the current state of main
238
+ repo.tag!("v1", ref: "main", message: "Launch version")
239
+
240
+ # More editing happens on main...
241
+ repo.commit!(message: "Post-launch tweaks", fields: { body: "..." })
242
+
243
+ # Readers still see v1 until you publish
244
+ repo.publish!("v1")
245
+
246
+ # post.title and post.body now reflect v1
247
+ # Your views just do: @post.title — no versioning API needed
248
+
249
+ # Publish latest tag
250
+ repo.publish!
251
+ ```
252
+
253
+ ### Log
254
+
255
+ ```ruby
256
+ log = repo.log("main") # => Log (Enumerable)
257
+ log = repo.log("main", limit: 10) # => last 10
258
+
259
+ # Log is enumerable
260
+ log.map(&:message) # => ["Added step 2", "First draft"]
261
+ log.first # => most recent Commit
262
+ log.to_text # => "* a1b2c3 Added step 2\n* f4e5d6 First draft"
263
+
264
+ # Commit objects
265
+ commit = log.first
266
+ commit.sha # => "a1b2c3..."
267
+ commit.message # => "Added step 2"
268
+ commit.author # => #<User id: 1, name: "Alice">
269
+ commit.committed_at # => 2026-03-21 14:30:00 UTC
270
+ commit.parents # => [#<Commit ...>]
271
+ commit.merge? # => false
272
+ commit.root? # => false
273
+ ```
274
+
275
+ ## How it works
276
+
277
+ Revisable uses the same conceptual model as git, minus the parts you don't need in a database:
278
+
279
+ | Git concept | Revisable equivalent | Notes |
280
+ |---|---|---|
281
+ | Blob | `revisable_blobs` | SHA-256 content-addressed. Identical content = one row. |
282
+ | Tree | — | Skipped. Fields are flat, not nested directories. |
283
+ | Commit | `revisable_commits` | Polymorphic author, parent links, scoped to a record. |
284
+ | Tree entries | `revisable_commit_fields` | Each commit stores a blob SHA for every revisable field. |
285
+ | Ref | `revisable_refs` | Branches and tags. Compare-and-swap updates for concurrency. |
286
+ | Packfile | — | Not needed. Postgres handles storage. |
287
+ | Index / staging | — | Not needed. Every `commit!` is direct. |
288
+
289
+ Each commit stores **full state** (a blob SHA for every field), not deltas. This means you can read any commit without walking history. Unchanged fields reuse the same blob SHA, so storage cost is minimal.
290
+
291
+ ## Schema
292
+
293
+ Revisable creates 5 tables. Run `rails generate revisable:install` to get the migration:
294
+
295
+ - **`revisable_blobs`** — content-addressed text storage
296
+ - **`revisable_commits`** — commit metadata, polymorphic to any model
297
+ - **`revisable_commit_parents`** — parent links (supports merge commits with 2+ parents)
298
+ - **`revisable_commit_fields`** — maps each commit × field to a blob
299
+ - **`revisable_refs`** — branches and tags per record
300
+
301
+ All tables are polymorphic — one set of tables serves every model that uses `revisable`.
302
+
303
+ ## Concurrency
304
+
305
+ Branch refs use compare-and-swap updates. If two writers commit to the same branch simultaneously, the second one gets a `Revisable::StaleRefError` and can retry. At CMS-level write volumes this is rarely hit, but it's there.
306
+
307
+ ## License
308
+
309
+ MIT
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "rails/generators/active_record"
5
+
6
+ module Revisable
7
+ class InstallGenerator < Rails::Generators::Base
8
+ include ActiveRecord::Generators::Migration
9
+
10
+ source_root File.expand_path("templates", __dir__)
11
+
12
+ def create_migration_file
13
+ migration_template "create_revisable_tables.rb.erb", "db/migrate/create_revisable_tables.rb"
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,57 @@
1
+ class CreateRevisableTables < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
2
+ def change
3
+ create_table :revisable_blobs, id: :bigint do |t|
4
+ t.string :sha, null: false, index: { unique: true }
5
+ t.text :data, null: false
6
+ t.integer :size, null: false
7
+ t.timestamps
8
+ end
9
+
10
+ create_table :revisable_commits, id: :bigint do |t|
11
+ t.string :sha, null: false, index: { unique: true }
12
+ t.string :versionable_type, null: false
13
+ t.bigint :versionable_id, null: false
14
+ t.string :author_type
15
+ t.bigint :author_id
16
+ t.string :message, null: false
17
+ t.datetime :committed_at, null: false
18
+ t.timestamps
19
+ end
20
+
21
+ add_index :revisable_commits, [:versionable_type, :versionable_id]
22
+ add_index :revisable_commits, [:author_type, :author_id]
23
+
24
+ create_table :revisable_commit_parents, id: :bigint do |t|
25
+ t.string :commit_sha, null: false
26
+ t.string :parent_sha, null: false
27
+ t.integer :position, null: false, default: 0
28
+ end
29
+
30
+ add_index :revisable_commit_parents, :commit_sha
31
+ add_index :revisable_commit_parents, :parent_sha
32
+
33
+ create_table :revisable_commit_fields, id: :bigint do |t|
34
+ t.string :commit_sha, null: false
35
+ t.string :field_name, null: false
36
+ t.string :blob_sha, null: false
37
+ end
38
+
39
+ add_index :revisable_commit_fields, :commit_sha
40
+ add_index :revisable_commit_fields, :blob_sha
41
+ add_index :revisable_commit_fields, [:commit_sha, :field_name], unique: true
42
+
43
+ create_table :revisable_refs, id: :bigint do |t|
44
+ t.string :versionable_type, null: false
45
+ t.bigint :versionable_id, null: false
46
+ t.string :name, null: false
47
+ t.string :ref_type, null: false
48
+ t.string :commit_sha, null: false
49
+ t.string :message
50
+ t.timestamps
51
+ end
52
+
53
+ add_index :revisable_refs, [:versionable_type, :versionable_id, :ref_type, :name],
54
+ unique: true, name: "index_revisable_refs_on_versionable_and_type_and_name"
55
+ add_index :revisable_refs, :commit_sha
56
+ end
57
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Revisable
4
+ module ActiveRecord
5
+ module ControllerHelpers
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ around_action :revisable_set_author, if: :revisable_author
10
+ end
11
+
12
+ private
13
+
14
+ def revisable_set_author(&block)
15
+ Revisable::CurrentAuthor.with(revisable_author, &block)
16
+ end
17
+
18
+ # Override in your controller to provide the current user
19
+ # Example:
20
+ # def revisable_author
21
+ # current_user
22
+ # end
23
+ def revisable_author
24
+ nil
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest/sha2"
4
+
5
+ module Revisable
6
+ class Blob < ActiveRecord::Base
7
+ self.table_name = "revisable_blobs"
8
+
9
+ validates :sha, presence: true, uniqueness: true
10
+ validates :data, presence: true
11
+ validates :size, presence: true
12
+
13
+ def self.store(content)
14
+ content = content.to_s
15
+ sha = compute_sha(content)
16
+
17
+ find_or_create_by!(sha: sha) do |blob|
18
+ blob.data = content
19
+ blob.size = content.bytesize
20
+ end
21
+ end
22
+
23
+ def self.compute_sha(content)
24
+ Digest::SHA256.hexdigest(content.to_s)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Revisable
4
+ class Branch
5
+ attr_reader :ref
6
+
7
+ def initialize(ref)
8
+ @ref = ref
9
+ end
10
+
11
+ def name
12
+ ref.name
13
+ end
14
+
15
+ def commit
16
+ ref.commit
17
+ end
18
+
19
+ def commit_sha
20
+ ref.commit_sha
21
+ end
22
+
23
+ def tip(fields:)
24
+ Snapshot.new(commit: commit, fields: fields)
25
+ end
26
+
27
+ def to_s
28
+ name
29
+ end
30
+
31
+ def ==(other)
32
+ other.is_a?(Branch) && name == other.name && commit_sha == other.commit_sha
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest/sha2"
4
+
5
+ module Revisable
6
+ class Commit < ActiveRecord::Base
7
+ self.table_name = "revisable_commits"
8
+
9
+ belongs_to :versionable, polymorphic: true
10
+ belongs_to :author, polymorphic: true, optional: true
11
+
12
+ has_many :commit_fields,
13
+ class_name: "Revisable::CommitField",
14
+ foreign_key: :commit_sha,
15
+ primary_key: :sha,
16
+ dependent: :destroy
17
+
18
+ has_many :parent_links,
19
+ class_name: "Revisable::CommitParent",
20
+ foreign_key: :commit_sha,
21
+ primary_key: :sha,
22
+ dependent: :destroy
23
+
24
+ validates :sha, presence: true, uniqueness: true
25
+ validates :message, presence: true
26
+
27
+ def parents
28
+ Commit.where(sha: parent_links.order(:position).pluck(:parent_sha))
29
+ end
30
+
31
+ def parent_shas
32
+ parent_links.order(:position).pluck(:parent_sha)
33
+ end
34
+
35
+ def root?
36
+ parent_links.empty?
37
+ end
38
+
39
+ def merge?
40
+ parent_links.count > 1
41
+ end
42
+
43
+ def snapshot(fields:)
44
+ Snapshot.new(commit: self, fields: fields)
45
+ end
46
+
47
+ def field_set
48
+ @field_set ||= begin
49
+ blobs = commit_fields.includes(:blob).each_with_object({}) do |cf, hash|
50
+ hash[cf.field_name.to_sym] = cf.blob
51
+ end
52
+ FieldSet.new(blobs)
53
+ end
54
+ end
55
+
56
+ def self.build_sha(parent_shas:, field_blobs:, message:, timestamp:)
57
+ parts = [
58
+ "parents:#{parent_shas.sort.join(',')}",
59
+ "fields:#{field_blobs.sort.map { |k, v| "#{k}:#{v}" }.join(',')}",
60
+ "message:#{message}",
61
+ "timestamp:#{timestamp.iso8601}"
62
+ ]
63
+ Digest::SHA256.hexdigest(parts.join("\n"))
64
+ end
65
+ end
66
+
67
+ class CommitParent < ActiveRecord::Base
68
+ self.table_name = "revisable_commit_parents"
69
+
70
+ belongs_to :commit,
71
+ class_name: "Revisable::Commit",
72
+ foreign_key: :commit_sha,
73
+ primary_key: :sha
74
+ end
75
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Revisable
4
+ class CommitField < ActiveRecord::Base
5
+ self.table_name = "revisable_commit_fields"
6
+
7
+ belongs_to :commit,
8
+ class_name: "Revisable::Commit",
9
+ foreign_key: :commit_sha,
10
+ primary_key: :sha
11
+
12
+ belongs_to :blob,
13
+ class_name: "Revisable::Blob",
14
+ foreign_key: :blob_sha,
15
+ primary_key: :sha
16
+
17
+ validates :field_name, presence: true
18
+ end
19
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Revisable
4
+ module CurrentAuthor
5
+ def self.set(author)
6
+ Thread.current[:revisable_current_author] = author
7
+ end
8
+
9
+ def self.get
10
+ Thread.current[:revisable_current_author]
11
+ end
12
+
13
+ def self.clear
14
+ Thread.current[:revisable_current_author] = nil
15
+ end
16
+
17
+ def self.with(author)
18
+ previous = get
19
+ set(author)
20
+ yield
21
+ ensure
22
+ set(previous)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Revisable
4
+ class Diff
5
+ include Enumerable
6
+
7
+ attr_reader :commit_a, :commit_b
8
+
9
+ def initialize(commit_a:, commit_b:, versionable_fields:)
10
+ @commit_a = commit_a
11
+ @commit_b = commit_b
12
+ @fields_a = commit_a&.field_set || FieldSet.empty
13
+ @fields_b = commit_b&.field_set || FieldSet.empty
14
+ @versionable_fields = versionable_fields
15
+
16
+ @fields = build_field_diffs
17
+ end
18
+
19
+ def field(name)
20
+ @fields[name.to_sym]
21
+ end
22
+
23
+ def each(&block)
24
+ @fields.each(&block)
25
+ end
26
+
27
+ def changed?
28
+ @fields.values.any?(&:changed?)
29
+ end
30
+
31
+ def changed_fields
32
+ @fields.values.select(&:changed?)
33
+ end
34
+
35
+ def to_text
36
+ @fields.values
37
+ .select(&:changed?)
38
+ .map(&:to_text)
39
+ .join("\n\n")
40
+ end
41
+
42
+ def to_html
43
+ @fields.values
44
+ .select(&:changed?)
45
+ .map(&:to_html)
46
+ .join("\n")
47
+ end
48
+
49
+ private
50
+
51
+ def build_field_diffs
52
+ @versionable_fields.each_with_object({}) do |field, hash|
53
+ field = field.to_sym
54
+ hash[field] = FieldDiff.new(
55
+ field_name: field,
56
+ blob_a: @fields_a[field],
57
+ blob_b: @fields_b[field]
58
+ )
59
+ end
60
+ end
61
+ end
62
+ end