Class: Mbeditor::ModelGraphService

Inherits:
Object
  • Object
show all
Defined in:
app/services/mbeditor/model_graph_service.rb

Overview

Builds a graph of the host app's ActiveRecord models and the associations between them, for the Models sidebar tab.

Reads the associations by reflection rather than by parsing model files. mbeditor already runs inside the host app, so reflect_on_all_associations is right there — and it resolves class_name:, through:, polymorphic and inverse sides correctly, which regex or AST parsing of has_many lines silently gets wrong. (ruby-lsp cannot help here: its index models constants and methods, not associations.)

Deliberately never touches the database connection. Reflections are pure metadata, so this works on an app whose database isn't running or migrated; column counts are only filled in when a connection happens to be live.

Constant Summary collapse

IGNORED_NAMESPACES =

Rails' own models — ActiveStorage, ActionText, SolidQueue, schema migrations — are not what you opened this tab to look at.

%w[
  ActiveRecord ActiveStorage ActionText ActionMailbox
  SolidQueue SolidCache SolidCable
].freeze
DEFAULT_MAX_MODELS =

Cap on models drawn. 300 was chosen before the layout could handle that many; a schema slightly over it silently lost models and only said "(truncated)". Raised, and configurable for anything larger — the cost of a bigger graph is now the browser's rendering, not the layout.

1000
MAX_COLUMNS_SENT =

Only the first few columns travel: the diagram box shows that many and the full list is a click away in the schema modal, which fetches its own data from /model_schema.

8

Class Method Summary collapse

Class Method Details

.call(workspace_root) ⇒ Object



42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'app/services/mbeditor/model_graph_service.rb', line 42

def call(workspace_root)
  root = workspace_root.to_s
  fingerprint = fingerprint_for(root)

  cached = MUTEX.synchronize { @cache }
  return cached[:payload] if cached && cached[:fingerprint] == fingerprint && cached[:root] == root

  payload = build(root)
  payload[:fingerprint] = fingerprint
  MUTEX.synchronize { @cache = { root: root, fingerprint: fingerprint, payload: payload } }
  write_artifacts(root, payload)
  payload
end

.fingerprint_for(root) ⇒ Object

A cheap stand-in for "have the models changed": the newest mtime and the file count across the model and migration directories. Avoids eager-loading the app just to decide whether the cache is stale.



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
# File 'app/services/mbeditor/model_graph_service.rb', line 66

def fingerprint_for(root)
  dirs = [File.join(root, "app", "models"), File.join(root, "db", "migrate")]
  newest = 0
  count = 0
  dirs.each do |dir|
    next unless File.directory?(dir)

    Dir.glob(File.join(dir, "**", "*.rb")).each do |f|
      count += 1
      mtime = File.mtime(f).to_i
      newest = mtime if mtime > newest
    end
  end
  "#{newest}:#{count}"
rescue StandardError
  "unknown"
end

.invalidate(_root = nil) ⇒ Object

Only needed for an explicit refresh: ordinary saves are picked up by the fingerprint below, so there is no file-change hook to keep in sync.



58
59
60
61
# File 'app/services/mbeditor/model_graph_service.rb', line 58

def invalidate(_root = nil)
  MUTEX.synchronize { @cache = nil }
  nil
end

.max_modelsObject



31
32
33
34
# File 'app/services/mbeditor/model_graph_service.rb', line 31

def self.max_models
  value = Mbeditor.configuration.model_graph_max_models.to_i
  value.positive? ? value : DEFAULT_MAX_MODELS
end