Class: Mistri::Workspace::ActiveRecord

Inherits:
Object
  • Object
show all
Defined in:
lib/mistri/workspace/active_record.rb

Overview

Documents in the host's own database, through a model class the host supplies, optionally scoped (per session, per tenant). Not auto-required; load it with require "mistri/workspace/active_record".

The model needs a non-null primary key plus non-null path, content, and scope columns, with a unique index whose columns are exactly the scope plus path. Atomic edits use that index plus a short row-locking transaction; no lock_version column is required. A migration to copy:

create_table :mistri_documents do |t|
t.string :session_id, null: false
t.string :path, null: false, limit: 512
t.text :content, size: :medium, null: false
t.timestamps
end
add_index :mistri_documents, [:session_id, :path], unique: true

Instance Method Summary collapse

Constructor Details

#initialize(model, scope: {}) ⇒ ActiveRecord

Returns a new instance of ActiveRecord.

Raises:

  • (ArgumentError)


27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# File 'lib/mistri/workspace/active_record.rb', line 27

def initialize(model, scope: {})
  raise ArgumentError, "scope must be a Hash" unless scope.is_a?(Hash)

  names = scope.keys.map(&:to_s)
  if names.uniq.length != names.length
    raise ArgumentError, "scope keys must name distinct columns"
  end

  reserved = names & %w[path content]
  unless reserved.empty?
    raise ArgumentError, "scope cannot contain #{reserved.map(&:inspect).join(" or ")}"
  end

  @model = model
  @scope = own_scope(scope)
end

Instance Method Details

#atomic_writes?Boolean

Returns:

  • (Boolean)


55
56
57
58
59
# File 'lib/mistri/workspace/active_record.rb', line 55

def atomic_writes?
  return @atomic_writes if defined?(@atomic_writes)

  @atomic_writes = atomic_storage?
end

#compare_and_write(path, content, expected_revision:) ⇒ Object



67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/mistri/workspace/active_record.rb', line 67

def compare_and_write(path, content, expected_revision:)
  verify_atomic_context!
  key = path.to_s
  return create_if_absent(key, content) if expected_revision.nil?

  @model.transaction do
    record = @model.lock.find_by(**@scope, path: key)
    actual = record && Snapshot.for(record.content.to_s).revision
    unless actual == expected_revision
      raise WorkspaceConflictError.new(
        key, expected_revision:, actual_revision: actual
      )
    end

    record.content = content.to_s
    record.save!
    committed_snapshot(record, key)
  end
end

#delete(path) ⇒ Object



87
88
89
90
# File 'lib/mistri/workspace/active_record.rb', line 87

def delete(path)
  @model.where(**@scope, path: path.to_s).delete_all
  nil
end

#list(prefix = nil) ⇒ Object



92
93
94
95
# File 'lib/mistri/workspace/active_record.rb', line 92

def list(prefix = nil)
  paths = @model.where(**@scope).pluck(:path).sort
  prefix ? paths.select { |p| p.start_with?(prefix.to_s) } : paths
end

#read(path) ⇒ Object



44
45
46
# File 'lib/mistri/workspace/active_record.rb', line 44

def read(path)
  @model.where(**@scope, path: path.to_s).pick(:content)
end

#snapshot(path) ⇒ Object



61
62
63
64
65
# File 'lib/mistri/workspace/active_record.rb', line 61

def snapshot(path)
  verify_atomic_context!
  content = relation(path).pick(:content)
  content.nil? ? nil : Snapshot.for(content.to_s)
end

#write(path, content) ⇒ Object



48
49
50
51
52
53
# File 'lib/mistri/workspace/active_record.rb', line 48

def write(path, content)
  record = @model.find_or_initialize_by(**@scope, path: path.to_s)
  record.content = content.to_s
  record.save!
  nil
end