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
MAX_MODELS =
300
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



33
34
35
36
37
38
39
40
41
42
43
44
45
# File 'app/services/mbeditor/model_graph_service.rb', line 33

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.



57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'app/services/mbeditor/model_graph_service.rb', line 57

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.



49
50
51
52
# File 'app/services/mbeditor/model_graph_service.rb', line 49

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