activecypher 0.2.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 +4 -4
- data/lib/active_cypher/base.rb +1 -1
- data/lib/active_cypher/bolt/session.rb +62 -50
- data/lib/active_cypher/bolt/transaction.rb +92 -87
- data/lib/active_cypher/connection_adapters/abstract_bolt_adapter.rb +40 -32
- data/lib/active_cypher/connection_adapters/neo4j_adapter.rb +1 -1
- data/lib/active_cypher/cypher_config.rb +12 -0
- data/lib/active_cypher/generators/node_generator.rb +32 -3
- data/lib/active_cypher/generators/relationship_generator.rb +29 -2
- data/lib/active_cypher/generators/templates/cypher_databases.yml +9 -21
- data/lib/active_cypher/generators/templates/node.rb.erb +10 -6
- data/lib/active_cypher/generators/templates/relationship.rb.erb +7 -6
- data/lib/active_cypher/instrumentation.rb +186 -0
- data/lib/active_cypher/model/connection_owner.rb +15 -0
- data/lib/active_cypher/model/core.rb +58 -4
- data/lib/active_cypher/model/countable.rb +10 -3
- data/lib/active_cypher/model/destruction.rb +17 -10
- data/lib/active_cypher/model/persistence.rb +28 -8
- data/lib/active_cypher/model/querying.rb +5 -1
- data/lib/active_cypher/relation.rb +10 -2
- data/lib/active_cypher/version.rb +1 -1
- data/lib/cyrel/clause/set.rb +20 -10
- data/lib/cyrel/expression/property_access.rb +2 -0
- data/lib/cyrel/plus.rb +11 -0
- data/lib/cyrel/query.rb +1 -0
- data/lib/cyrel.rb +77 -18
- metadata +3 -2
- data/lib/tasks/active_cypher_tasks.rake +0 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7d529adab6eb5478a435e3061d317f742373617d8460b14f545d2b66b03cd37d
|
4
|
+
data.tar.gz: 35726eeafd44cc915e3731b3c4557e63f6ace655b87c28761e1ebc426d989af1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0cdb35c5ebeb4d5774a2ff203b6322ef26e1f1abcdaac97b93cea64125322454fdd95b6a91117ced49ea5391815b96e84f274f96fd57725f0de65b1916aef7cf
|
7
|
+
data.tar.gz: 258b065dc249ac957e7eed9025663dbbb12e7f0927dd7555793365bbbbbad36ffff4458aba48f99baedfecc02c214a490ba5e2cf68a9244d27a284291951f79f
|
data/lib/active_cypher/base.rb
CHANGED
@@ -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
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
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
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
#
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
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
|
-
|
185
|
-
|
192
|
+
instrument('session.reset') do
|
193
|
+
# Mark the current transaction as no longer active
|
194
|
+
complete_transaction(@current_transaction)
|
186
195
|
|
187
|
-
|
188
|
-
|
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
|
-
|
194
|
-
|
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
|
-
|
197
|
-
|
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
|
-
#
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
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
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
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
|
-
|
106
|
-
|
107
|
-
|
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
|
-
|
110
|
-
|
113
|
+
# Read response to COMMIT
|
114
|
+
response = @connection.read_message
|
111
115
|
|
112
|
-
|
113
|
-
|
114
|
-
|
116
|
+
case response
|
117
|
+
when Messaging::Success
|
118
|
+
# COMMIT succeeded
|
115
119
|
|
116
|
-
|
120
|
+
@state = :committed
|
117
121
|
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
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
|
-
|
126
|
-
|
129
|
+
# Mark transaction as completed in the session
|
130
|
+
@session.complete_transaction(self, new_bookmarks)
|
127
131
|
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
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
|
-
|
136
|
-
|
139
|
+
# Mark transaction as completed in the session
|
140
|
+
@session.complete_transaction(self)
|
137
141
|
|
138
|
-
|
139
|
-
|
140
|
-
|
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
|
-
|
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
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
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
|
-
|
49
|
-
|
50
|
-
|
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
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
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
|
|
@@ -15,6 +15,18 @@ module ActiveCypher
|
|
15
15
|
env ||= defined?(Rails) ? Rails.env : ENV.fetch('CY_ENV', 'development')
|
16
16
|
file = Pathname.new(path || default_path)
|
17
17
|
|
18
|
+
# Handle missing config file gracefully
|
19
|
+
unless file.exist?
|
20
|
+
# If requesting all configs, return empty hash
|
21
|
+
return {}.with_indifferent_access if name.to_s == '*'
|
22
|
+
# If silent missing is set, return nil for specific connection
|
23
|
+
return nil if ENV['ACTIVE_CYPHER_SILENT_MISSING'] == 'true'
|
24
|
+
|
25
|
+
# Otherwise, raise a descriptive error
|
26
|
+
raise "Could not load ActiveCypher configuration. No such file - #{file}. " \
|
27
|
+
"Please run 'rails generate active_cypher:install' to create the configuration file."
|
28
|
+
end
|
29
|
+
|
18
30
|
## ------------------------------------------------------------
|
19
31
|
## 1. Parse YAML using the same algorithm Rails::Application#config_for
|
20
32
|
## uses (shared‑section merge, ERB, symbolize_keys, etc.)
|
@@ -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
|
-
|
20
|
-
|
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(
|
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
|
-
|
23
|
-
|
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
|