rails_graph 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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