rails_graph 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.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +17 -0
  4. data/.ruby-version +1 -0
  5. data/.tool-versions +1 -0
  6. data/CHANGELOG.md +5 -0
  7. data/CODE_OF_CONDUCT.md +84 -0
  8. data/Gemfile +12 -0
  9. data/Gemfile.lock +142 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +89 -0
  12. data/Rakefile +12 -0
  13. data/graph.svg +1 -0
  14. data/lib/rails_graph/commands/build_graph.rb +113 -0
  15. data/lib/rails_graph/commands/export_graph.rb +21 -0
  16. data/lib/rails_graph/configuration.rb +26 -0
  17. data/lib/rails_graph/error.rb +5 -0
  18. data/lib/rails_graph/exporters/base.rb +7 -0
  19. data/lib/rails_graph/exporters/cypher.rb +62 -0
  20. data/lib/rails_graph/exporters/json.rb +16 -0
  21. data/lib/rails_graph/exporters/neo4j.rb +26 -0
  22. data/lib/rails_graph/graph/entity.rb +21 -0
  23. data/lib/rails_graph/graph/graph.rb +43 -0
  24. data/lib/rails_graph/graph/node.rb +26 -0
  25. data/lib/rails_graph/graph/nodes/abstract_model.rb +27 -0
  26. data/lib/rails_graph/graph/nodes/column.rb +31 -0
  27. data/lib/rails_graph/graph/nodes/model.rb +44 -0
  28. data/lib/rails_graph/graph/nodes/virtual_model.rb +23 -0
  29. data/lib/rails_graph/graph/relationship.rb +34 -0
  30. data/lib/rails_graph/graph/relationships/association.rb +74 -0
  31. data/lib/rails_graph/graph/relationships/attribute.rb +23 -0
  32. data/lib/rails_graph/graph/relationships/inheritance.rb +21 -0
  33. data/lib/rails_graph/helpers/associations.rb +31 -0
  34. data/lib/rails_graph/helpers/models.rb +13 -0
  35. data/lib/rails_graph/helpers/options_parser.rb +21 -0
  36. data/lib/rails_graph/railtie.rb +14 -0
  37. data/lib/rails_graph/tasks/rails_graph.rake +27 -0
  38. data/lib/rails_graph/version.rb +5 -0
  39. data/lib/rails_graph.rb +33 -0
  40. data/sig/rails_graph.rbs +4 -0
  41. metadata +140 -0
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../graph/graph"
4
+ require_relative "../graph/node"
5
+ require_relative "../graph/relationship"
6
+
7
+ require_relative "../graph/nodes/abstract_model"
8
+ require_relative "../graph/nodes/column"
9
+ require_relative "../graph/nodes/model"
10
+ require_relative "../graph/nodes/virtual_model"
11
+
12
+ require_relative "../graph/relationships/association"
13
+ require_relative "../graph/relationships/attribute"
14
+ require_relative "../graph/relationships/inheritance"
15
+
16
+ require_relative "../helpers/associations"
17
+ require_relative "../helpers/models"
18
+
19
+ module RailsGraph
20
+ module Commands
21
+ class BuildGraph
22
+ def self.call(configuration:)
23
+ new(configuration: configuration).call
24
+ end
25
+
26
+ def call
27
+ polymorphic_node = RailsGraph::Graph::Nodes::VirtualModel.new("PolymorphicModel")
28
+ graph.add_node(polymorphic_node)
29
+
30
+ active_record_base_node = RailsGraph::Graph::Nodes::AbstractModel.new(ActiveRecord::Base)
31
+ graph.add_node(active_record_base_node)
32
+
33
+ build_model_nodes
34
+ build_associations_relationships
35
+ build_column_nodes if configuration.columns?
36
+ build_inheritance_relationships if configuration.inheritance?
37
+
38
+ graph
39
+ end
40
+
41
+ private
42
+
43
+ attr_reader :configuration, :classes, :graph
44
+
45
+ def initialize(configuration:)
46
+ @configuration = configuration
47
+ @classes = ActiveRecord::Base.descendants + configuration.include_classes
48
+ @graph = RailsGraph::Graph::Graph.new
49
+ end
50
+
51
+ def build_model_nodes
52
+ classes.each do |model|
53
+ if model.abstract_class
54
+ node = RailsGraph::Graph::Nodes::AbstractModel.new(model)
55
+ graph.add_node(node)
56
+ next
57
+ end
58
+
59
+ node = RailsGraph::Graph::Nodes::Model.new(model)
60
+ graph.add_node(node)
61
+ end
62
+ end
63
+
64
+ def build_column_nodes
65
+ processed = Hash.new(false)
66
+
67
+ classes.each do |model|
68
+ next if model.attribute_names.empty?
69
+
70
+ identifier = RailsGraph::Helpers::Models.identifier(model)
71
+ node = graph.node(identifier)
72
+
73
+ next if processed[node.id]
74
+
75
+ processed[node.id] = true
76
+
77
+ model.columns.each do |column|
78
+ column_node = RailsGraph::Graph::Nodes::Column.new(column)
79
+ graph.add_node(column_node)
80
+
81
+ relationship = RailsGraph::Graph::Relationships::Attribute.new(node, column_node)
82
+ graph.add_relationship(relationship)
83
+ end
84
+ end
85
+ end
86
+
87
+ def build_inheritance_relationships
88
+ classes.each do |model|
89
+ identifier = RailsGraph::Helpers::Models.identifier(model)
90
+ node = graph.node(identifier)
91
+
92
+ superclass_node_identifier = RailsGraph::Helpers::Models.identifier(model.superclass)
93
+ superclass_node = graph.node(superclass_node_identifier)
94
+
95
+ relationship = RailsGraph::Graph::Relationships::Inheritance.new(node, superclass_node)
96
+ graph.add_relationship(relationship)
97
+ end
98
+ end
99
+
100
+ def build_associations_relationships
101
+ classes.each do |model|
102
+ model.reflect_on_all_associations.each do |association|
103
+ source_node = RailsGraph::Helpers::Associations.source_node(graph, association)
104
+ target_node = RailsGraph::Helpers::Associations.target_node(graph, association)
105
+
106
+ relationship = RailsGraph::Graph::Relationships::Association.new(association, source_node, target_node)
107
+ graph.add_relationship(relationship)
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../exporters/cypher"
4
+ require_relative "../exporters/json"
5
+ require_relative "../exporters/neo4j"
6
+
7
+ module RailsGraph
8
+ module Commands
9
+ class ExportGraph
10
+ EXPORTERS = {
11
+ cypher: RailsGraph::Exporters::Cypher,
12
+ json: RailsGraph::Exporters::Json,
13
+ neo4j: RailsGraph::Exporters::Neo4j
14
+ }.freeze
15
+
16
+ def self.call(graph:, format: :cypher, **options)
17
+ EXPORTERS[format].export(graph: graph, **options)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsGraph
4
+ class Configuration
5
+ attr_reader :include_classes
6
+ attr_writer :columns, :inheritance
7
+
8
+ def initialize
9
+ @include_classes = []
10
+ @columns = false
11
+ @inheritance = true
12
+ end
13
+
14
+ def include_classes=(include_classes)
15
+ @include_classes = Array(include_classes)
16
+ end
17
+
18
+ def columns?
19
+ @columns
20
+ end
21
+
22
+ def inheritance?
23
+ @inheritance
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsGraph
4
+ class Error < StandardError; end
5
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsGraph
4
+ module Exporters
5
+ class Base; end
6
+ end
7
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module RailsGraph
6
+ module Exporters
7
+ class Cypher < Base
8
+ BASE_CYPHER = "MATCH (n) DETACH DELETE n;"
9
+
10
+ def self.export(graph:, filename:)
11
+ queries = build_queries(graph)
12
+
13
+ File.write(filename, queries.join("\n"), mode: "w")
14
+ end
15
+
16
+ def self.build_queries(graph)
17
+ queries = []
18
+ queries << BASE_CYPHER
19
+
20
+ graph.nodes.each { |node| queries << create_node_cypher(node) }
21
+ graph.relationships.each { |relationship| queries << create_relationship_cypher(relationship) }
22
+
23
+ queries
24
+ end
25
+
26
+ def self.node_ref(node)
27
+ "ref_#{node.id.gsub("-", "_")}"
28
+ end
29
+
30
+ def self.format_properties(item)
31
+ output = "name: '#{item.name}'"
32
+
33
+ item.properties.each do |k, v|
34
+ formatted_value = case v
35
+ when String, Symbol then "'#{v}'"
36
+ when nil then "null"
37
+ else v.to_s
38
+ end
39
+
40
+ output += ", #{k}: #{formatted_value}"
41
+ end
42
+
43
+ output
44
+ end
45
+
46
+ def self.create_node_cypher(node)
47
+ ref = node_ref(node)
48
+ labels = node.labels.join(":")
49
+ properties = format_properties(node)
50
+
51
+ "CREATE (#{ref}:#{labels} {#{properties}})"
52
+ end
53
+
54
+ def self.create_relationship_cypher(relationship)
55
+ source_ref = node_ref(relationship.source)
56
+ target_ref = node_ref(relationship.target)
57
+
58
+ "CREATE (#{source_ref})-[:#{relationship.label} {#{format_properties(relationship)}}]->(#{target_ref})"
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+ require "json"
5
+
6
+ module RailsGraph
7
+ module Exporters
8
+ class Json < Base
9
+ def self.export(graph:, filename:)
10
+ json = graph.to_json
11
+
12
+ File.write(filename, json, mode: "w")
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+ require "neo4j_ruby_driver"
5
+
6
+ module RailsGraph
7
+ module Exporters
8
+ class Neo4j < Base
9
+ def self.export(graph:, host:, username:, password:)
10
+ auth = ::Neo4j::Driver::AuthTokens.basic(username, password)
11
+
12
+ ::Neo4j::Driver::GraphDatabase.driver(host, auth) do |driver|
13
+ driver.session do |session|
14
+ queries = Cypher.build_queries(graph)
15
+ queries.shift
16
+ queries = queries.join("\n")
17
+
18
+ session.write_transaction do |tx|
19
+ tx.run(queries, message: "Success!")
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
5
+ module RailsGraph
6
+ module Graph
7
+ class Entity
8
+ attr_reader :id, :name, :properties
9
+
10
+ def initialize(name:, id: SecureRandom.uuid, properties: {})
11
+ @id = id
12
+ @name = name
13
+ @properties = properties
14
+ end
15
+
16
+ def identifier
17
+ id
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsGraph
4
+ module Graph
5
+ class Graph
6
+ def initialize(nodes: {}, relationships: {})
7
+ @nodes = nodes
8
+ @relationships = relationships
9
+ end
10
+
11
+ def add_node(node)
12
+ @nodes[node.identifier] = node
13
+ end
14
+
15
+ def nodes
16
+ @nodes.values
17
+ end
18
+
19
+ def node(identifier)
20
+ @nodes[identifier]
21
+ end
22
+
23
+ def add_relationship(relationship)
24
+ @relationships[relationship.identifier] = relationship
25
+ end
26
+
27
+ def relationships
28
+ @relationships.values.flatten
29
+ end
30
+
31
+ def relationship(identifier)
32
+ @relationships[identifier]
33
+ end
34
+
35
+ def as_json(_options = nil)
36
+ {
37
+ nodes: nodes,
38
+ relationships: relationships
39
+ }
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "entity"
4
+
5
+ module RailsGraph
6
+ module Graph
7
+ class Node < Entity
8
+ attr_reader :labels
9
+
10
+ def initialize(labels: [], **opts)
11
+ @labels = Array(labels)
12
+
13
+ super(**opts)
14
+ end
15
+
16
+ def as_json(_options = nil)
17
+ {
18
+ id: id,
19
+ labels: labels,
20
+ name: name,
21
+ properties: properties
22
+ }
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../node"
4
+
5
+ module RailsGraph
6
+ module Graph
7
+ module Nodes
8
+ class AbstractModel < Node
9
+ attr_reader :model
10
+
11
+ def initialize(model)
12
+ @model = model
13
+
14
+ super(
15
+ labels: "AbstractModel",
16
+ name: model.name,
17
+ properties: {}
18
+ )
19
+ end
20
+
21
+ def identifier
22
+ RailsGraph::Helpers::Models.identifier(model)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../node"
4
+
5
+ module RailsGraph
6
+ module Graph
7
+ module Nodes
8
+ class Column < Node
9
+ attr_reader :column
10
+
11
+ def initialize(column)
12
+ @column = column
13
+
14
+ super(labels: "Column", name: column.name, properties: build_properties)
15
+ end
16
+
17
+ private
18
+
19
+ def build_properties
20
+ {
21
+ nullable: column.null || false,
22
+ comment: column.comment,
23
+ default: column.default,
24
+ type: column.type,
25
+ sql_type: column.sql_type
26
+ }
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../node"
4
+
5
+ module RailsGraph
6
+ module Graph
7
+ module Nodes
8
+ class Model < Node
9
+ attr_reader :model
10
+
11
+ def initialize(model)
12
+ @model = model
13
+
14
+ super(labels: "Model", name: model.name, properties: build_properties)
15
+ end
16
+
17
+ def identifier
18
+ RailsGraph::Helpers::Models.identifier(model)
19
+ end
20
+
21
+ private
22
+
23
+ def build_properties
24
+ {
25
+ table_name: model.table_name,
26
+ table_exists: table_exists?,
27
+ columns_count: model.attribute_names.count,
28
+ db_indexes_count: db_indexes_count,
29
+ primary_key: model.primary_key,
30
+ full_name: model.to_s
31
+ }
32
+ end
33
+
34
+ def table_exists?
35
+ ActiveRecord::Base.connection.table_exists?(model.table_name)
36
+ end
37
+
38
+ def db_indexes_count
39
+ ActiveRecord::Base.connection.indexes(model.table_name).count
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../node"
4
+
5
+ module RailsGraph
6
+ module Graph
7
+ module Nodes
8
+ class VirtualModel < Node
9
+ def initialize(name)
10
+ super(
11
+ labels: "VirtualModel",
12
+ name: name,
13
+ properties: {}
14
+ )
15
+ end
16
+
17
+ def identifier
18
+ name
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "entity"
4
+
5
+ module RailsGraph
6
+ module Graph
7
+ class Relationship < Entity
8
+ attr_reader :source, :target, :label
9
+
10
+ def initialize(source:, target:, label:, **opts)
11
+ @source = source
12
+ @target = target
13
+ @label = label
14
+
15
+ super(**opts)
16
+ end
17
+
18
+ def identifier
19
+ "#{label}##{source.identifier}##{target.identifier}##{name}"
20
+ end
21
+
22
+ def as_json(_options = nil)
23
+ {
24
+ id: id,
25
+ label: label,
26
+ name: name,
27
+ source: source.id,
28
+ target: target.id,
29
+ properties: properties
30
+ }
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../relationship"
4
+
5
+ module RailsGraph
6
+ module Graph
7
+ module Relationships
8
+ class Association < Relationship
9
+ attr_reader :association
10
+
11
+ def initialize(association, source, target)
12
+ @association = association
13
+
14
+ super(
15
+ source: source,
16
+ target: target,
17
+ label: association.macro.to_s.camelize,
18
+ name: association.name.to_s,
19
+ properties: build_properties
20
+ )
21
+ end
22
+
23
+ private
24
+
25
+ def build_properties
26
+ {
27
+ polymorphic: polymorphic?,
28
+ through: through?,
29
+ foreign_key: foreign_key,
30
+ foreign_type: foreign_type,
31
+ type: type,
32
+ dependent: dependent,
33
+ class_name: class_name,
34
+ optional: optional?
35
+ }
36
+ end
37
+
38
+ def class_name
39
+ name = association.polymorphic? ? association.class_name : association.klass.name
40
+
41
+ name.delete_prefix("::")
42
+ end
43
+
44
+ def polymorphic?
45
+ association.polymorphic? || false
46
+ end
47
+
48
+ def through?
49
+ association.options[:through] || false
50
+ end
51
+
52
+ def foreign_key
53
+ association.foreign_key.to_s
54
+ end
55
+
56
+ def foreign_type
57
+ association.foreign_type.to_s
58
+ end
59
+
60
+ def type
61
+ association.type
62
+ end
63
+
64
+ def dependent
65
+ association.options[:dependent]
66
+ end
67
+
68
+ def optional?
69
+ association.options[:optional] || false
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../relationship"
4
+
5
+ module RailsGraph
6
+ module Graph
7
+ module Relationships
8
+ class Attribute < Relationship
9
+ def initialize(source, target)
10
+ super(
11
+ source: source,
12
+ target: target,
13
+ label: "HasAttribute",
14
+ name: target.name,
15
+ properties: {
16
+ primary_key: source.properties[:primary_key] == target.name
17
+ }
18
+ )
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../relationship"
4
+
5
+ module RailsGraph
6
+ module Graph
7
+ module Relationships
8
+ class Inheritance < Relationship
9
+ def initialize(source, target)
10
+ super(
11
+ source: source,
12
+ target: target,
13
+ label: "InheritsFrom",
14
+ name: target.name,
15
+ properties: {}
16
+ )
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsGraph
4
+ module Helpers
5
+ module Associations
6
+ module_function
7
+
8
+ def source_identifier(association)
9
+ RailsGraph::Helpers::Models.identifier(association.active_record)
10
+ end
11
+
12
+ def source_node(graph, association)
13
+ identifier = source_identifier(association)
14
+
15
+ graph.node(identifier)
16
+ end
17
+
18
+ def target_identifier(association)
19
+ return "PolymorphicModel" if association.polymorphic?
20
+
21
+ RailsGraph::Helpers::Models.identifier(association.klass)
22
+ end
23
+
24
+ def target_node(graph, association)
25
+ identifier = target_identifier(association)
26
+
27
+ graph.node(identifier)
28
+ end
29
+ end
30
+ end
31
+ end