activecypher 0.3.0 → 0.5.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 56e09b9d8193d2c4c745c1973bb420ba83854cbe0b578d5d37d4e215c96c7687
4
- data.tar.gz: a53e722376ff97ae59c6fc0735c16eb15b9e2fe89fe45cda6d42c5d45237a97f
3
+ metadata.gz: 7d529adab6eb5478a435e3061d317f742373617d8460b14f545d2b66b03cd37d
4
+ data.tar.gz: 35726eeafd44cc915e3731b3c4557e63f6ace655b87c28761e1ebc426d989af1
5
5
  SHA512:
6
- metadata.gz: bd26475b4774f41f37b047c558707c7fe5282718c8970f1a24148c3f6ad57186ad7b0823051e06d420d80373605f8d28721b7f934dd64e0110b05050af6af7f9
7
- data.tar.gz: 20b688e0788fe8dd84a51471ee2125d8ce8082304d4e9db724d4970b0e27da638fd4e541030982b582716d5ad1c1f86d1e88c3fbb3605437bab05c96a5e2b832
6
+ metadata.gz: 0cdb35c5ebeb4d5774a2ff203b6322ef26e1f1abcdaac97b93cea64125322454fdd95b6a91117ced49ea5391815b96e84f274f96fd57725f0de65b1916aef7cf
7
+ data.tar.gz: 258b065dc249ac957e7eed9025663dbbb12e7f0927dd7555793365bbbbbad36ffff4458aba48f99baedfecc02c214a490ba5e2cf68a9244d27a284291951f79f
@@ -11,12 +11,12 @@ module ActiveCypher
11
11
  include Logging
12
12
 
13
13
  # Let's just include every concern we can find, because why not.
14
+ include Model::Querying # Must be before Core so Core can override its methods
14
15
  include Model::Core
15
16
  include Model::Attributes
16
17
  include Model::ConnectionOwner
17
18
  include Model::Callbacks
18
19
  include Model::Persistence
19
- include Model::Querying
20
20
  include Model::ConnectionHandling
21
21
  include Model::Destruction
22
22
  include Model::Abstract
@@ -1,12 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'async'
4
-
5
4
  module ActiveCypher
6
5
  module Bolt
7
6
  # A Session is the primary unit of work in the Bolt Protocol.
8
7
  # It maintains a connection to the database server and allows running queries.
9
8
  class Session
9
+ include Instrumentation
10
10
  attr_reader :connection, :database
11
11
 
12
12
  def initialize(connection, database: nil)
@@ -34,13 +34,15 @@ module ActiveCypher
34
34
  # For Memgraph, explicitly set db to nil
35
35
  db = nil if @connection.adapter.is_a?(ConnectionAdapters::MemgraphAdapter)
36
36
 
37
- if @current_transaction&.active?
38
- # If we have an active transaction, run the query within it
39
- @current_transaction.run(query, parameters)
40
- else
41
- # Auto-transaction mode: each query gets its own transaction
42
- run_transaction(mode, db: db) do |tx|
43
- tx.run(query, parameters)
37
+ instrument_query(query, parameters, context: 'Session#run', metadata: { mode: mode, db: db }) do
38
+ if @current_transaction&.active?
39
+ # If we have an active transaction, run the query within it
40
+ @current_transaction.run(query, parameters)
41
+ else
42
+ # Auto-transaction mode: each query gets its own transaction
43
+ run_transaction(mode, db: db) do |tx|
44
+ tx.run(query, parameters)
45
+ end
44
46
  end
45
47
  end
46
48
  end
@@ -55,40 +57,46 @@ module ActiveCypher
55
57
  def begin_transaction(db: nil, access_mode: :write, tx_timeout: nil, tx_metadata: nil)
56
58
  raise ConnectionError, 'Already in a transaction' if @current_transaction&.active?
57
59
 
58
- # Send BEGIN message with appropriate metadata
59
- begin_meta = {}
60
- # For Memgraph, NEVER set a database name - it doesn't support them
61
- if @connection.adapter.is_a?(ConnectionAdapters::MemgraphAdapter)
62
- # Explicitly don't set db for Memgraph
63
- begin_meta['adapter'] = 'memgraph'
64
- # Force db to nil for Memgraph
65
- nil
66
- elsif db || @database
67
- begin_meta['db'] = db || @database
68
- end
69
- begin_meta['mode'] = access_mode == :read ? 'r' : 'w'
70
- begin_meta['tx_timeout'] = tx_timeout if tx_timeout
71
- begin_meta['tx_metadata'] = tx_metadata if tx_metadata
72
- begin_meta['bookmarks'] = @bookmarks if @bookmarks&.any?
73
-
74
- begin_msg = Messaging::Begin.new(begin_meta)
75
- @connection.write_message(begin_msg)
76
-
77
- # Read response to BEGIN
78
- response = @connection.read_message
79
-
80
- case response
81
- when Messaging::Success
82
- # BEGIN succeeded, create a new transaction
83
- @current_transaction = Transaction.new(self, @bookmarks, response.metadata)
84
- when Messaging::Failure
85
- # BEGIN failed
86
- code = response.metadata['code']
87
- message = response.metadata['message']
88
- @connection.reset!
89
- raise QueryError, "Failed to begin transaction: #{code} - #{message}"
90
- else
91
- raise ProtocolError, "Unexpected response to BEGIN: #{response.class}"
60
+ metadata = { access_mode: access_mode }
61
+ metadata[:db] = db if db
62
+ metadata[:timeout] = tx_timeout if tx_timeout
63
+
64
+ instrument_transaction(:begin, nil, metadata: metadata) do
65
+ # Send BEGIN message with appropriate metadata
66
+ begin_meta = {}
67
+ # For Memgraph, NEVER set a database name - it doesn't support them
68
+ if @connection.adapter.is_a?(ConnectionAdapters::MemgraphAdapter)
69
+ # Explicitly don't set db for Memgraph
70
+ begin_meta['adapter'] = 'memgraph'
71
+ # Force db to nil for Memgraph
72
+ nil
73
+ elsif db || @database
74
+ begin_meta['db'] = db || @database
75
+ end
76
+ begin_meta['mode'] = access_mode == :read ? 'r' : 'w'
77
+ begin_meta['tx_timeout'] = tx_timeout if tx_timeout
78
+ begin_meta['tx_metadata'] = tx_metadata if tx_metadata
79
+ begin_meta['bookmarks'] = @bookmarks if @bookmarks&.any?
80
+
81
+ begin_msg = Messaging::Begin.new(begin_meta)
82
+ @connection.write_message(begin_msg)
83
+
84
+ # Read response to BEGIN
85
+ response = @connection.read_message
86
+
87
+ case response
88
+ when Messaging::Success
89
+ # BEGIN succeeded, create a new transaction
90
+ @current_transaction = Transaction.new(self, @bookmarks, response.metadata)
91
+ when Messaging::Failure
92
+ # BEGIN failed
93
+ code = response.metadata['code']
94
+ message = response.metadata['message']
95
+ @connection.reset!
96
+ raise QueryError, "Failed to begin transaction: #{code} - #{message}"
97
+ else
98
+ raise ProtocolError, "Unexpected response to BEGIN: #{response.class}"
99
+ end
92
100
  end
93
101
  end
94
102
 
@@ -181,20 +189,24 @@ module ActiveCypher
181
189
  def reset
182
190
  return if @current_transaction.nil?
183
191
 
184
- # Mark the current transaction as no longer active
185
- complete_transaction(@current_transaction)
192
+ instrument('session.reset') do
193
+ # Mark the current transaction as no longer active
194
+ complete_transaction(@current_transaction)
186
195
 
187
- # Reset the connection
188
- @connection.reset!
196
+ # Reset the connection
197
+ @connection.reset!
198
+ end
189
199
  end
190
200
 
191
201
  # Close the session and any active transaction.
192
202
  def close
193
- # If there's an active transaction, try to roll it back
194
- @current_transaction&.rollback if @current_transaction&.active?
203
+ instrument('session.close') do
204
+ # If there's an active transaction, try to roll it back
205
+ @current_transaction&.rollback if @current_transaction&.active?
195
206
 
196
- # Mark current transaction as complete
197
- complete_transaction(@current_transaction) if @current_transaction
207
+ # Mark current transaction as complete
208
+ complete_transaction(@current_transaction) if @current_transaction
209
+ end
198
210
  end
199
211
  end
200
212
  end
@@ -4,6 +4,7 @@ module ActiveCypher
4
4
  module Bolt
5
5
  # Manages transaction state (BEGIN/COMMIT/ROLLBACK) and runs queries within a transaction.
6
6
  class Transaction
7
+ include Instrumentation
7
8
  attr_reader :bookmarks, :metadata
8
9
 
9
10
  # Initializes a new Transaction instance.
@@ -30,66 +31,68 @@ module ActiveCypher
30
31
  # Ensure query is a string
31
32
  query_str = query.is_a?(String) ? query : query.to_s
32
33
 
33
- # Send RUN message
34
- run_metadata = {}
35
- run_msg = Messaging::Run.new(query_str, parameters, run_metadata)
36
- @connection.write_message(run_msg)
37
-
38
- # Read response to RUN
39
- response = @connection.read_message
40
- qid = -1
41
- fields = []
42
-
43
- case response
44
- when Messaging::Success
45
- # RUN succeeded, extract metadata
46
-
47
- qid = response.metadata['qid'] if response.metadata.key?('qid')
48
- fields = response.metadata['fields'] if response.metadata.key?('fields')
49
-
50
- # Send PULL to get all records (-1 = all)
51
- pull_metadata = { 'n' => -1 }
52
- pull_metadata['qid'] = qid if qid != -1
53
- pull_msg = Messaging::Pull.new(pull_metadata)
54
- @connection.write_message(pull_msg)
55
-
56
- # Process PULL response(s)
57
- records = []
58
- summary_metadata = {}
59
-
60
- # Read messages until we get a SUCCESS (or FAILURE)
61
- loop do
62
- msg = @connection.read_message
63
- case msg
64
- when Messaging::Record
65
- # Store record with raw values - processing will happen in the adapter
66
- records << msg.values
67
- when Messaging::Success
68
- # Final SUCCESS with summary metadata
69
- summary_metadata = msg.metadata
70
- break # Done processing results
71
- when Messaging::Failure
72
- connection.reset!
73
- # PULL failed - transaction is now failed
74
- @state = :failed
75
- code = msg.metadata['code']
76
- message = msg.metadata['message']
77
- raise QueryError, "Query execution failed: #{code} - #{message}"
78
- else
79
- raise ProtocolError, "Unexpected message type: #{msg.class}"
34
+ instrument_query(query_str, parameters, context: 'Transaction#run', metadata: { transaction_id: object_id }) do
35
+ # Send RUN message
36
+ run_metadata = {}
37
+ run_msg = Messaging::Run.new(query_str, parameters, run_metadata)
38
+ @connection.write_message(run_msg)
39
+
40
+ # Read response to RUN
41
+ response = @connection.read_message
42
+ qid = -1
43
+ fields = []
44
+
45
+ case response
46
+ when Messaging::Success
47
+ # RUN succeeded, extract metadata
48
+
49
+ qid = response.metadata['qid'] if response.metadata.key?('qid')
50
+ fields = response.metadata['fields'] if response.metadata.key?('fields')
51
+
52
+ # Send PULL to get all records (-1 = all)
53
+ pull_metadata = { 'n' => -1 }
54
+ pull_metadata['qid'] = qid if qid != -1
55
+ pull_msg = Messaging::Pull.new(pull_metadata)
56
+ @connection.write_message(pull_msg)
57
+
58
+ # Process PULL response(s)
59
+ records = []
60
+ summary_metadata = {}
61
+
62
+ # Read messages until we get a SUCCESS (or FAILURE)
63
+ loop do
64
+ msg = @connection.read_message
65
+ case msg
66
+ when Messaging::Record
67
+ # Store record with raw values - processing will happen in the adapter
68
+ records << msg.values
69
+ when Messaging::Success
70
+ # Final SUCCESS with summary metadata
71
+ summary_metadata = msg.metadata
72
+ break # Done processing results
73
+ when Messaging::Failure
74
+ connection.reset!
75
+ # PULL failed - transaction is now failed
76
+ @state = :failed
77
+ code = msg.metadata['code']
78
+ message = msg.metadata['message']
79
+ raise QueryError, "Query execution failed: #{code} - #{message}"
80
+ else
81
+ raise ProtocolError, "Unexpected message type: #{msg.class}"
82
+ end
80
83
  end
81
- end
82
84
 
83
- # Create and return Result object
84
- Result.new(fields, records, summary_metadata, qid)
85
- when Messaging::Failure
86
- # RUN failed - transaction is now failed
87
- @state = :failed
88
- code = response.metadata['code']
89
- message = response.metadata['message']
90
- raise QueryError, "Query execution failed: #{code} - #{message}"
91
- else
92
- raise ProtocolError, "Unexpected response to RUN: #{response.class}"
85
+ # Create and return Result object
86
+ Result.new(fields, records, summary_metadata, qid)
87
+ when Messaging::Failure
88
+ # RUN failed - transaction is now failed
89
+ @state = :failed
90
+ code = response.metadata['code']
91
+ message = response.metadata['message']
92
+ raise QueryError, "Query execution failed: #{code} - #{message}"
93
+ else
94
+ raise ProtocolError, "Unexpected response to RUN: #{response.class}"
95
+ end
93
96
  end
94
97
  rescue ConnectionError
95
98
  @state = :failed
@@ -102,42 +105,44 @@ module ActiveCypher
102
105
  def commit
103
106
  raise ConnectionError, "Cannot commit a #{@state} transaction" unless @state == :active
104
107
 
105
- # Send COMMIT message
106
- commit_msg = Messaging::Commit.new
107
- @connection.write_message(commit_msg)
108
+ instrument_transaction(:commit, object_id) do
109
+ # Send COMMIT message
110
+ commit_msg = Messaging::Commit.new
111
+ @connection.write_message(commit_msg)
108
112
 
109
- # Read response to COMMIT
110
- response = @connection.read_message
113
+ # Read response to COMMIT
114
+ response = @connection.read_message
111
115
 
112
- case response
113
- when Messaging::Success
114
- # COMMIT succeeded
116
+ case response
117
+ when Messaging::Success
118
+ # COMMIT succeeded
115
119
 
116
- @state = :committed
120
+ @state = :committed
117
121
 
118
- # Extract bookmarks if any
119
- new_bookmarks = []
120
- if response.metadata.key?('bookmark')
121
- new_bookmarks = [response.metadata['bookmark']]
122
- @bookmarks = new_bookmarks
123
- end
122
+ # Extract bookmarks if any
123
+ new_bookmarks = []
124
+ if response.metadata.key?('bookmark')
125
+ new_bookmarks = [response.metadata['bookmark']]
126
+ @bookmarks = new_bookmarks
127
+ end
124
128
 
125
- # Mark transaction as completed in the session
126
- @session.complete_transaction(self, new_bookmarks)
129
+ # Mark transaction as completed in the session
130
+ @session.complete_transaction(self, new_bookmarks)
127
131
 
128
- new_bookmarks
129
- when Messaging::Failure
130
- # COMMIT failed
131
- @state = :failed
132
- code = response.metadata['code']
133
- message = response.metadata['message']
132
+ new_bookmarks
133
+ when Messaging::Failure
134
+ # COMMIT failed
135
+ @state = :failed
136
+ code = response.metadata['code']
137
+ message = response.metadata['message']
134
138
 
135
- # Mark transaction as completed in the session
136
- @session.complete_transaction(self)
139
+ # Mark transaction as completed in the session
140
+ @session.complete_transaction(self)
137
141
 
138
- raise QueryError, "Failed to commit transaction: #{code} - #{message}"
139
- else
140
- raise ProtocolError, "Unexpected response to COMMIT: #{response.class}"
142
+ raise QueryError, "Failed to commit transaction: #{code} - #{message}"
143
+ else
144
+ raise ProtocolError, "Unexpected response to COMMIT: #{response.class}"
145
+ end
141
146
  end
142
147
  rescue ConnectionError
143
148
  @state = :failed
@@ -155,7 +160,7 @@ module ActiveCypher
155
160
  # If already committed or rolled back, do nothing
156
161
  return if @state == :committed || @state == :rolled_back
157
162
 
158
- begin
163
+ instrument_transaction(:rollback, object_id) do
159
164
  # Send ROLLBACK message
160
165
  rollback_msg = Messaging::Rollback.new
161
166
  @connection.write_message(rollback_msg)
@@ -8,6 +8,7 @@ module ActiveCypher
8
8
  # Concrete subclasses must provide protocol_handler_class, validate_connection, and execute_cypher.
9
9
  # It's like ActiveRecord::ConnectionAdapter, but for weirdos like me who use graph databases.
10
10
  class AbstractBoltAdapter < AbstractAdapter
11
+ include Instrumentation
11
12
  attr_reader :connection
12
13
 
13
14
  # Establish a connection if not already active.
@@ -15,29 +16,31 @@ module ActiveCypher
15
16
  def connect
16
17
  return true if active?
17
18
 
18
- # Determine host and port from config
19
- host, port = if config[:uri]
20
- # Legacy URI format
21
- uri = URI(config[:uri])
22
- [uri.host, uri.port || 7687]
23
- else
24
- # New URL format via ConnectionUrlResolver
25
- [config[:host] || 'localhost', config[:port] || 7687]
26
- end
27
-
28
- # Prepare auth token
29
- auth = if config[:username]
30
- { scheme: 'basic', principal: config[:username], credentials: config[:password] }
31
- else
32
- { scheme: 'none' }
33
- end
34
-
35
- @connection = Bolt::Connection.new(
36
- host, port, self,
37
- auth_token: auth, timeout_seconds: config.fetch(:timeout, 15)
38
- )
39
- @connection.connect
40
- validate_connection
19
+ instrument_connection(:connect, config) do
20
+ # Determine host and port from config
21
+ host, port = if config[:uri]
22
+ # Legacy URI format
23
+ uri = URI(config[:uri])
24
+ [uri.host, uri.port || 7687]
25
+ else
26
+ # New URL format via ConnectionUrlResolver
27
+ [config[:host] || 'localhost', config[:port] || 7687]
28
+ end
29
+
30
+ # Prepare auth token
31
+ auth = if config[:username]
32
+ { scheme: 'basic', principal: config[:username], credentials: config[:password] }
33
+ else
34
+ { scheme: 'none' }
35
+ end
36
+
37
+ @connection = Bolt::Connection.new(
38
+ host, port, self,
39
+ auth_token: auth, timeout_seconds: config.fetch(:timeout, 15)
40
+ )
41
+ @connection.connect
42
+ validate_connection
43
+ end
41
44
  end
42
45
 
43
46
  # Connection health check. If this returns false, you're probably in trouble.
@@ -45,9 +48,11 @@ module ActiveCypher
45
48
 
46
49
  # Clean disconnection. Resets the internal state.
47
50
  def disconnect
48
- @connection&.close
49
- @connection = nil
50
- true
51
+ instrument_connection(:disconnect) do
52
+ @connection&.close
53
+ @connection = nil
54
+ true
55
+ end
51
56
  end
52
57
 
53
58
  # Runs a Cypher query via Bolt session.
@@ -55,11 +60,14 @@ module ActiveCypher
55
60
  def run(cypher, params = {}, context: 'Query', db: nil, access_mode: :write)
56
61
  connect
57
62
  logger.debug { "[#{context}] #{cypher} #{params.inspect}" }
58
- session = Bolt::Session.new(connection, default_db: db)
59
- result = session.run(cypher, prepare_params(params), access_mode:)
60
- rows = result.respond_to?(:to_a) ? result.to_a : result
61
- session.close
62
- rows
63
+
64
+ instrument_query(cypher, params, context: context, metadata: { db: db, access_mode: access_mode }) do
65
+ session = Bolt::Session.new(connection, database: db)
66
+ result = session.run(cypher, prepare_params(params), access_mode:)
67
+ rows = result.respond_to?(:to_a) ? result.to_a : result
68
+ session.close
69
+ rows
70
+ end
63
71
  end
64
72
 
65
73
  # Convert access mode to database-specific format
@@ -84,7 +92,7 @@ module ActiveCypher
84
92
  # you will be publicly shamed by a NotImplementedError.
85
93
  def protocol_handler_class = raise(NotImplementedError)
86
94
  def validate_connection = raise(NotImplementedError)
87
- def execute_cypher(*) = raise(NotImplementedError)
95
+ def execute_cypher(*) = raise(NotImplementedError, "#{self.class} must implement #execute_cypher")
88
96
 
89
97
  private
90
98
 
@@ -16,7 +16,7 @@ module ActiveCypher
16
16
  end
17
17
 
18
18
  # Explicit TX helpers — optional but handy.
19
- def begin_transaction = (@tx = @connection.session.begin_transaction)
19
+ def begin_transaction(**) = (@tx = @connection.session.begin_transaction(**))
20
20
  def commit_transaction(_) = @tx&.commit
21
21
  def rollback_transaction(_) = @tx&.rollback
22
22
 
@@ -23,7 +23,8 @@ module ActiveCypher
23
23
  return nil if ENV['ACTIVE_CYPHER_SILENT_MISSING'] == 'true'
24
24
 
25
25
  # Otherwise, raise a descriptive error
26
- raise "Could not load ActiveCypher configuration. No such file - #{file}. Please run 'rails generate active_cypher:install' to create the configuration file."
26
+ raise "Could not load ActiveCypher configuration. No such file - #{file}. " \
27
+ "Please run 'rails generate active_cypher:install' to create the configuration file."
27
28
  end
28
29
 
29
30
  ## ------------------------------------------------------------
@@ -7,6 +7,11 @@ module ActiveCypher
7
7
  module Generators
8
8
  class NodeGenerator < Rails::Generators::NamedBase
9
9
  source_root File.expand_path('templates', __dir__)
10
+ class_option :suffix, type: :string,
11
+ desc: 'Suffix for the node class (default: Node)',
12
+ default: 'Node'
13
+
14
+ check_class_collision suffix: 'Node'
10
15
 
11
16
  argument :attributes, type: :array,
12
17
  default: [], banner: 'name:type name:type'
@@ -16,16 +21,40 @@ module ActiveCypher
16
21
  default: ''
17
22
 
18
23
  def create_node_file
19
- template 'node.rb.erb',
20
- File.join('app/graph', class_path, "#{file_name}.rb")
24
+ check_runtime_class_collision
25
+ template 'node.rb.erb', File.join('app/graph', class_path, "#{file_name}.rb")
21
26
  end
22
27
 
23
28
  private
24
29
 
30
+ def check_runtime_class_collision
31
+ suffix = node_suffix
32
+ base = name.camelize
33
+ class_name_with_suffix = base.end_with?(suffix) ? base : "#{base}#{suffix}"
34
+ return unless class_name_with_suffix.safe_constantize
35
+
36
+ raise Thor::Error, "Class collision: #{class_name_with_suffix} is already defined"
37
+ end
38
+
39
+ def node_suffix
40
+ options[:suffix] || 'Node'
41
+ end
42
+
43
+ def class_name
44
+ base = super
45
+ base.end_with?(node_suffix) ? base : "#{base}#{node_suffix}"
46
+ end
47
+
48
+ def file_name
49
+ base = super
50
+ suffix = "_#{node_suffix.underscore}"
51
+ base.end_with?(suffix) ? base : "#{base}#{suffix}"
52
+ end
53
+
25
54
  # helper for ERB
26
55
  def labels_list
27
56
  lbls = options[:labels].split(',').map(&:strip).reject(&:blank?)
28
- lbls.empty? ? [class_name.gsub(/Node$/, '')] : lbls
57
+ lbls.empty? ? [class_name.gsub(/#{node_suffix}$/, '')] : lbls
29
58
  end
30
59
  end
31
60
  end
@@ -7,6 +7,9 @@ module ActiveCypher
7
7
  module Generators
8
8
  class RelationshipGenerator < Rails::Generators::NamedBase
9
9
  source_root File.expand_path('templates', __dir__)
10
+ class_option :suffix, type: :string,
11
+ desc: 'Suffix for the relationship class (default: Rel)',
12
+ default: 'Rel'
10
13
 
11
14
  argument :attributes, type: :array,
12
15
  default: [], banner: 'name:type name:type'
@@ -19,12 +22,36 @@ module ActiveCypher
19
22
  desc: 'Cypher relationship type (defaults to class name)'
20
23
 
21
24
  def create_relationship_file
22
- template 'relationship.rb.erb',
23
- File.join('app/graph', class_path, "#{file_name}.rb")
25
+ check_runtime_class_collision
26
+ template 'relationship.rb.erb', File.join('app/graph', class_path, "#{file_name}.rb")
24
27
  end
25
28
 
26
29
  private
27
30
 
31
+ def relationship_suffix
32
+ options[:suffix] || 'Rel'
33
+ end
34
+
35
+ def class_name
36
+ base = super
37
+ base.end_with?(relationship_suffix) ? base : "#{base}#{relationship_suffix}"
38
+ end
39
+
40
+ def file_name
41
+ base = super
42
+ suffix = "_#{relationship_suffix.underscore}"
43
+ base.end_with?(suffix) ? base : "#{base}#{suffix}"
44
+ end
45
+
46
+ def check_runtime_class_collision
47
+ suffix = relationship_suffix
48
+ base = name.camelize
49
+ class_name_with_suffix = base.end_with?(suffix) ? base : "#{base}#{suffix}"
50
+ return unless class_name_with_suffix.safe_constantize.present?
51
+
52
+ raise Thor::Error, "Class collision: #{class_name_with_suffix} is already defined"
53
+ end
54
+
28
55
  def relationship_type
29
56
  (options[:type].presence || class_name).upcase
30
57
  end
@@ -1,10 +1,14 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  class <%= class_name %> < ApplicationGraphNode
3
- <% labels_list.each do |lbl| %>
4
+ <% if labels_list.any? -%>
5
+ <% labels_list.each do |lbl| -%>
4
6
  label :<%= lbl %>
5
- <% end %>
6
-
7
- <% attributes.each do |attr| -%>
8
- attribute :<%= attr.name %>, :<%= attr.type || "string" %>
9
- <% end -%>
7
+ <% end -%>
8
+ <% end -%>
9
+ <% if attributes.any? -%>
10
+ <% attributes.each do |attr| -%>
11
+ attribute :<%= attr.name %>, :<%= attr.type || "string" %>
12
+ <% end -%>
13
+ <% end -%>
10
14
  end