activecypher 0.0.0 → 0.2.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 (97) hide show
  1. checksums.yaml +4 -4
  2. data/lib/active_cypher/associations/collection_proxy.rb +144 -0
  3. data/lib/active_cypher/associations.rb +537 -0
  4. data/lib/active_cypher/base.rb +47 -0
  5. data/lib/active_cypher/bolt/connection.rb +525 -0
  6. data/lib/active_cypher/bolt/driver.rb +144 -0
  7. data/lib/active_cypher/bolt/handlers.rb +10 -0
  8. data/lib/active_cypher/bolt/message_reader.rb +100 -0
  9. data/lib/active_cypher/bolt/message_writer.rb +53 -0
  10. data/lib/active_cypher/bolt/messaging.rb +307 -0
  11. data/lib/active_cypher/bolt/packstream.rb +319 -0
  12. data/lib/active_cypher/bolt/result.rb +82 -0
  13. data/lib/active_cypher/bolt/session.rb +201 -0
  14. data/lib/active_cypher/bolt/transaction.rb +211 -0
  15. data/lib/active_cypher/bolt/version_encoding.rb +41 -0
  16. data/lib/active_cypher/bolt.rb +7 -0
  17. data/lib/active_cypher/connection_adapters/abstract_adapter.rb +75 -0
  18. data/lib/active_cypher/connection_adapters/abstract_bolt_adapter.rb +178 -0
  19. data/lib/active_cypher/connection_adapters/memgraph_adapter.rb +44 -0
  20. data/lib/active_cypher/connection_adapters/neo4j_adapter.rb +58 -0
  21. data/lib/active_cypher/connection_factory.rb +130 -0
  22. data/lib/active_cypher/connection_handler.rb +9 -0
  23. data/lib/active_cypher/connection_pool.rb +123 -0
  24. data/lib/active_cypher/connection_url_resolver.rb +137 -0
  25. data/lib/active_cypher/cypher_config.rb +50 -0
  26. data/lib/active_cypher/generators/install_generator.rb +23 -0
  27. data/lib/active_cypher/generators/node_generator.rb +32 -0
  28. data/lib/active_cypher/generators/relationship_generator.rb +33 -0
  29. data/lib/active_cypher/generators/templates/application_graph_node.rb +5 -0
  30. data/lib/active_cypher/generators/templates/application_graph_relationship.rb +5 -0
  31. data/lib/active_cypher/generators/templates/cypher_databases.yml +28 -0
  32. data/lib/active_cypher/generators/templates/node.rb.erb +10 -0
  33. data/lib/active_cypher/generators/templates/relationship.rb.erb +11 -0
  34. data/lib/active_cypher/logging.rb +44 -0
  35. data/lib/active_cypher/model/abstract.rb +87 -0
  36. data/lib/active_cypher/model/attributes.rb +24 -0
  37. data/lib/active_cypher/model/callbacks.rb +44 -0
  38. data/lib/active_cypher/model/connection_handling.rb +76 -0
  39. data/lib/active_cypher/model/connection_owner.rb +50 -0
  40. data/lib/active_cypher/model/core.rb +45 -0
  41. data/lib/active_cypher/model/countable.rb +30 -0
  42. data/lib/active_cypher/model/destruction.rb +49 -0
  43. data/lib/active_cypher/model/inspectable.rb +28 -0
  44. data/lib/active_cypher/model/persistence.rb +182 -0
  45. data/lib/active_cypher/model/querying.rb +67 -0
  46. data/lib/active_cypher/railtie.rb +34 -0
  47. data/lib/active_cypher/relation.rb +190 -0
  48. data/lib/active_cypher/relationship.rb +233 -0
  49. data/lib/active_cypher/runtime_registry.rb +8 -0
  50. data/lib/active_cypher/scoping.rb +97 -0
  51. data/lib/active_cypher/utils/logger.rb +100 -0
  52. data/lib/active_cypher/version.rb +5 -0
  53. data/lib/activecypher.rb +108 -0
  54. data/lib/cyrel/call_procedure.rb +29 -0
  55. data/lib/cyrel/clause/call.rb +46 -0
  56. data/lib/cyrel/clause/call_subquery.rb +40 -0
  57. data/lib/cyrel/clause/create.rb +33 -0
  58. data/lib/cyrel/clause/delete.rb +41 -0
  59. data/lib/cyrel/clause/limit.rb +33 -0
  60. data/lib/cyrel/clause/match.rb +40 -0
  61. data/lib/cyrel/clause/merge.rb +34 -0
  62. data/lib/cyrel/clause/order_by.rb +78 -0
  63. data/lib/cyrel/clause/remove.rb +75 -0
  64. data/lib/cyrel/clause/return.rb +90 -0
  65. data/lib/cyrel/clause/set.rb +97 -0
  66. data/lib/cyrel/clause/skip.rb +34 -0
  67. data/lib/cyrel/clause/where.rb +42 -0
  68. data/lib/cyrel/clause/with.rb +94 -0
  69. data/lib/cyrel/clause.rb +25 -0
  70. data/lib/cyrel/direction.rb +18 -0
  71. data/lib/cyrel/expression/alias.rb +27 -0
  72. data/lib/cyrel/expression/base.rb +101 -0
  73. data/lib/cyrel/expression/case.rb +45 -0
  74. data/lib/cyrel/expression/comparison.rb +60 -0
  75. data/lib/cyrel/expression/exists.rb +42 -0
  76. data/lib/cyrel/expression/function_call.rb +57 -0
  77. data/lib/cyrel/expression/literal.rb +33 -0
  78. data/lib/cyrel/expression/logical.rb +38 -0
  79. data/lib/cyrel/expression/operator.rb +27 -0
  80. data/lib/cyrel/expression/pattern_comprehension.rb +44 -0
  81. data/lib/cyrel/expression/property_access.rb +25 -0
  82. data/lib/cyrel/expression.rb +56 -0
  83. data/lib/cyrel/functions.rb +116 -0
  84. data/lib/cyrel/node.rb +397 -0
  85. data/lib/cyrel/parameterizable.rb +20 -0
  86. data/lib/cyrel/pattern/node.rb +66 -0
  87. data/lib/cyrel/pattern/path.rb +41 -0
  88. data/lib/cyrel/pattern/relationship.rb +74 -0
  89. data/lib/cyrel/pattern.rb +8 -0
  90. data/lib/cyrel/query.rb +497 -0
  91. data/lib/cyrel/return_only.rb +26 -0
  92. data/lib/cyrel/types/hash_type.rb +22 -0
  93. data/lib/cyrel/types/symbol_type.rb +13 -0
  94. data/lib/cyrel.rb +72 -0
  95. data/lib/tasks/active_cypher_tasks.rake +6 -0
  96. data/sig/activecypher.rbs +4 -0
  97. metadata +173 -10
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveCypher
4
+ # ConnectionFactory provides a simple API for creating connections to Cypher databases
5
+ # using the ConnectionUrlResolver to parse database URLs.
6
+ #
7
+ # This factory simplifies the process of creating database connections by:
8
+ # 1. Parsing connection URLs with ConnectionUrlResolver
9
+ # 2. Creating the appropriate adapter based on the URL
10
+ # 3. Establishing and configuring the connection with the right security settings
11
+ #
12
+ # Example:
13
+ # factory = ConnectionFactory.new("neo4j://user:pass@localhost:7687")
14
+ # driver = factory.create_driver
15
+ #
16
+ # driver.with_session do |session|
17
+ # result = session.run("RETURN 'Connected!' AS message")
18
+ # puts result.first[:message]
19
+ # end
20
+ class ConnectionFactory
21
+ # Initialize with a database URL
22
+ # @param url [String] A database connection URL
23
+ # @param options [Hash] Additional options for the connection
24
+ def initialize(url, options = {})
25
+ @url = url
26
+ @options = options
27
+ @config = resolve_url(url)
28
+ end
29
+
30
+ # Create a Bolt driver based on the parsed URL
31
+ # @param pool_size [Integer] Size of the connection pool
32
+ # @return [ActiveCypher::Bolt::Driver, nil] The configured driver or nil if URL is invalid
33
+ def create_driver(pool_size: 5)
34
+ return nil unless @config
35
+
36
+ # Create the adapter based on the resolved configuration
37
+ adapter = create_adapter
38
+ return nil unless adapter
39
+
40
+ # Create and configure the Bolt driver
41
+ uri = build_uri
42
+ auth_token = build_auth_token
43
+
44
+ ActiveCypher::Bolt::Driver.new(
45
+ uri: uri,
46
+ adapter: adapter,
47
+ auth_token: auth_token,
48
+ pool_size: pool_size
49
+ )
50
+ end
51
+
52
+ # Get the parsed configuration
53
+ # @return [Hash, nil] The parsed configuration or nil if URL is invalid
54
+ attr_reader :config
55
+
56
+ # Verify if the URL is valid and supported
57
+ # @return [Boolean] True if the URL is valid and supported
58
+ def valid?
59
+ !@config.nil?
60
+ end
61
+
62
+ private
63
+
64
+ # Resolve the URL into a configuration hash
65
+ def resolve_url(url)
66
+ resolver = ConnectionUrlResolver.new(url)
67
+ resolver.to_hash
68
+ end
69
+
70
+ # Create the appropriate adapter based on the parsed URL
71
+ def create_adapter
72
+ case @config[:adapter]
73
+ when 'neo4j'
74
+ create_neo4j_adapter
75
+ when 'memgraph'
76
+ create_memgraph_adapter
77
+ else
78
+ nil
79
+ end
80
+ end
81
+
82
+ # Create a Neo4j adapter
83
+ def create_neo4j_adapter
84
+ ConnectionAdapters::Neo4jAdapter.new({
85
+ uri: build_uri,
86
+ username: @config[:username],
87
+ password: @config[:password],
88
+ database: @config[:database]
89
+ })
90
+ end
91
+
92
+ # Create a Memgraph adapter
93
+ def create_memgraph_adapter
94
+ ConnectionAdapters::MemgraphAdapter.new({
95
+ uri: build_uri,
96
+ username: @config[:username],
97
+ password: @config[:password],
98
+ database: @config[:database]
99
+ })
100
+ rescue NameError
101
+ # Fall back to Neo4j adapter if Memgraph adapter is not available
102
+ ConnectionAdapters::Neo4jAdapter.new({
103
+ uri: build_uri,
104
+ username: @config[:username],
105
+ password: @config[:password],
106
+ database: @config[:database]
107
+ })
108
+ end
109
+
110
+ # Build the URI string with the appropriate scheme based on SSL settings
111
+ def build_uri
112
+ scheme = if @config[:ssl]
113
+ @config[:ssc] ? 'bolt+ssc' : 'bolt+s'
114
+ else
115
+ 'bolt'
116
+ end
117
+
118
+ "#{scheme}://#{@config[:host]}:#{@config[:port]}"
119
+ end
120
+
121
+ # Build the authentication token for the Bolt driver
122
+ def build_auth_token
123
+ {
124
+ scheme: 'basic',
125
+ principal: @config[:username],
126
+ credentials: @config[:password]
127
+ }
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveCypher
4
+ class ConnectionHandler
5
+ def initialize = @role_shard_map = Hash.new { |h, k| h[k] = {} }
6
+ def set(role, shard, pool) = (@role_shard_map[role][shard] = pool)
7
+ def pool(role, shard) = @role_shard_map.dig(role, shard)
8
+ end
9
+ end
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'concurrent/atomic/atomic_reference'
4
+ require 'timeout'
5
+
6
+ module ActiveCypher
7
+ class ConnectionPool
8
+ attr_reader :spec
9
+
10
+ def initialize(spec)
11
+ @spec = spec.symbolize_keys
12
+
13
+ # Set defaults for pool configuration
14
+ @spec[:pool_size] ||= ENV.fetch('BOLT_POOL_SIZE', 10).to_i
15
+ @spec[:pool_timeout] ||= ENV.fetch('BOLT_POOL_TIMEOUT', 5).to_i
16
+ @spec[:max_retries] ||= ENV.fetch('BOLT_MAX_RETRIES', 3).to_i
17
+
18
+ # Handle URL-based configuration if present
19
+ if @spec[:url] && !@spec.key?(:adapter)
20
+ resolver = ActiveCypher::ConnectionUrlResolver.new(@spec[:url])
21
+ resolved_config = resolver.to_hash
22
+
23
+ raise ArgumentError, "Invalid connection URL: #{@spec[:url]}" unless resolved_config
24
+
25
+ # Merge the resolved config with any additional options
26
+ @spec = resolved_config.merge(@spec.except(:url))
27
+
28
+ end
29
+
30
+ @conn_ref = Concurrent::AtomicReference.new # holds the adapter instance
31
+ @creation_mutex = Mutex.new # prevents multiple threads from creating connections simultaneously
32
+ @retry_count = Concurrent::AtomicReference.new(0)
33
+ end
34
+
35
+ # Returns a live adapter, initialising it once in a thread‑safe way.
36
+ def connection
37
+ # Fast path —already connected and alive
38
+ conn = @conn_ref.value
39
+ return conn if conn&.active?
40
+
41
+ # Use mutex for the slow path to prevent thundering herd
42
+ @creation_mutex.synchronize do
43
+ # Check again inside the mutex in case another thread created it
44
+ conn = @conn_ref.value
45
+ return conn if conn&.active?
46
+
47
+ # Slow path —create a new connection with retry logic
48
+ retries = 0
49
+ max_retries = @spec[:max_retries]
50
+
51
+ begin
52
+ new_conn = build_connection
53
+ @conn_ref.set(new_conn)
54
+ @retry_count.set(0) # Reset retry count on success
55
+ return new_conn
56
+ rescue StandardError => e
57
+ retries += 1
58
+ if retries <= max_retries
59
+ # Exponential backoff
60
+ sleep_time = 0.1 * (2**(retries - 1))
61
+ sleep(sleep_time)
62
+ retry
63
+ else
64
+ # Track persistent failures
65
+ @retry_count.update { |count| count + 1 }
66
+ raise ConnectionError, "Failed to establish connection after #{max_retries} attempts: #{e.message}"
67
+ end
68
+ end
69
+ end
70
+ end
71
+ alias checkout connection
72
+
73
+ # Check if the pool has a persistent connection issue
74
+ def troubled?
75
+ @retry_count.value >= @spec[:max_retries]
76
+ end
77
+
78
+ # Explicitly close and reset the connection
79
+ def disconnect
80
+ conn = @conn_ref.value
81
+ return unless conn
82
+
83
+ begin
84
+ conn.disconnect
85
+ rescue StandardError => e
86
+ # Log but don't raise to ensure cleanup continues
87
+ puts "Warning: Error disconnecting: #{e.message}" if ENV['DEBUG']
88
+ ensure
89
+ @conn_ref.set(nil)
90
+ end
91
+ end
92
+
93
+ private
94
+
95
+ def build_connection
96
+ adapter_name = @spec[:adapter]
97
+ raise ArgumentError, 'Missing adapter name in connection specification' unless adapter_name
98
+
99
+ adapter_class = ActiveCypher::ConnectionAdapters
100
+ .const_get("#{adapter_name}_adapter".camelize)
101
+
102
+ adapter = adapter_class.new(@spec)
103
+
104
+ # Use timeout to avoid hanging during connection
105
+ begin
106
+ Timeout.timeout(@spec[:pool_timeout]) do
107
+ adapter.connect
108
+ end
109
+ rescue Timeout::Error
110
+ begin
111
+ adapter.disconnect
112
+ rescue StandardError
113
+ nil
114
+ end
115
+ raise ConnectionError, "Connection timed out after #{@spec[:pool_timeout]} seconds"
116
+ end
117
+
118
+ adapter
119
+ rescue NameError
120
+ raise ActiveCypher::AdapterNotFoundError, "Could not find adapter class for '#{adapter_name}'"
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+ require 'cgi'
5
+
6
+ module ActiveCypher
7
+ # ConnectionUrlResolver accepts Cypher-based database URLs and converts them
8
+ # into a normalized configuration hash for adapter resolution.
9
+ #
10
+ # Supported URL prefixes:
11
+ # - neo4j://
12
+ # - neo4j+ssl://
13
+ # - neo4j+ssc://
14
+ # - memgraph://
15
+ # - memgraph+ssl://
16
+ # - memgraph+ssc://
17
+ #
18
+ # The output of to_hash follows a consistent pattern:
19
+ # {
20
+ # adapter: "neo4j", # or "memgraph"
21
+ # username: "user",
22
+ # password: "pass",
23
+ # host: "localhost",
24
+ # port: 7687,
25
+ # database: nil, # or optional
26
+ # ssl: true,
27
+ # ssc: false,
28
+ # options: {} # future-proof for params like '?timeout=30'
29
+ # }
30
+ class ConnectionUrlResolver
31
+ SUPPORTED_ADAPTERS = %w[neo4j memgraph].freeze
32
+ DEFAULT_PORT = 7687
33
+
34
+ # Initialize with a URL string
35
+ # @param url_string [String] A connection URL string
36
+ def initialize(url_string)
37
+ @url_string = url_string
38
+ @parsed = parse_url(url_string)
39
+ end
40
+
41
+ # Convert the URL to a normalized hash configuration
42
+ # @return [Hash] Configuration hash with adapter, host, port, etc.
43
+ def to_hash
44
+ return nil unless @parsed
45
+
46
+ {
47
+ adapter: @parsed[:adapter],
48
+ host: @parsed[:host],
49
+ port: @parsed[:port],
50
+ username: @parsed[:username],
51
+ password: @parsed[:password],
52
+ database: @parsed[:database],
53
+ ssl: @parsed[:ssl],
54
+ ssc: @parsed[:ssc],
55
+ options: @parsed[:options]
56
+ }
57
+ end
58
+
59
+ private
60
+
61
+ def parse_url(url_string)
62
+ return nil if url_string.nil? || url_string.empty?
63
+
64
+ # Extract scheme and potential modifiers (ssl, ssc)
65
+ scheme_parts = url_string.split('://', 2)
66
+ return nil if scheme_parts.size != 2
67
+
68
+ scheme = scheme_parts[0]
69
+ rest = scheme_parts[1].to_s
70
+
71
+ adapter, modifiers = extract_adapter_and_modifiers(scheme)
72
+ return nil unless adapter
73
+
74
+ # Parse the remaining part as a standard URI
75
+ uri_string = "#{adapter}://#{rest}"
76
+ begin
77
+ uri = URI.parse(uri_string)
78
+ rescue URI::InvalidURIError
79
+ return nil
80
+ end
81
+
82
+ # Extract query parameters, if any
83
+ options = extract_query_params(uri.query)
84
+
85
+ # Extract database from path, if present
86
+ database = uri.path.empty? ? nil : uri.path.sub(%r{^/}, '')
87
+ database = nil if database&.empty?
88
+
89
+ # The to_s conversion handles nil values
90
+ username = uri.user.to_s.empty? ? nil : CGI.unescape(uri.user)
91
+ password = uri.password.to_s.empty? ? nil : CGI.unescape(uri.password)
92
+
93
+ # When using SSC (self-signed certificates), SSL must also be enabled
94
+ use_ssl = modifiers.include?('ssl')
95
+ use_ssc = modifiers.include?('ssc')
96
+
97
+ # Self-signed certificates imply SSL is also enabled
98
+ use_ssl = true if use_ssc
99
+
100
+ {
101
+ adapter: adapter,
102
+ host: uri.host || 'localhost',
103
+ port: uri.port || DEFAULT_PORT,
104
+ username: username,
105
+ password: password,
106
+ database: database,
107
+ ssl: use_ssl,
108
+ ssc: use_ssc,
109
+ options: options
110
+ }
111
+ end
112
+
113
+ def extract_adapter_and_modifiers(scheme)
114
+ parts = scheme.split('+')
115
+ adapter = parts.shift
116
+
117
+ return nil unless SUPPORTED_ADAPTERS.include?(adapter)
118
+
119
+ modifiers = parts.select { |mod| %w[ssl ssc].include?(mod) }
120
+
121
+ # If there are parts that are neither the adapter nor valid modifiers, the URL is invalid
122
+ remaining_parts = parts - modifiers
123
+ return nil if remaining_parts.any?
124
+
125
+ [adapter, modifiers]
126
+ end
127
+
128
+ def extract_query_params(query_string)
129
+ return {} unless query_string
130
+
131
+ query_string.split('&').each_with_object({}) do |pair, hash|
132
+ key, value = pair.split('=', 2)
133
+ hash[key.to_sym] = value if key && !key.empty? && value && !value.empty?
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/active_cypher/cypher_config.rb
4
+ require 'active_support'
5
+
6
+ module ActiveCypher
7
+ module CypherConfig
8
+ #
9
+ # Read config/cypher_databases.yml the Rails‑native way and then
10
+ # pick a *named connection* (default :primary).
11
+ #
12
+ # Works **outside** Rails too by falling back to ActiveSupport::ConfigurationFile.
13
+ #
14
+ def self.for(name = :primary, env: nil, path: nil)
15
+ env ||= defined?(Rails) ? Rails.env : ENV.fetch('CY_ENV', 'development')
16
+ file = Pathname.new(path || default_path)
17
+
18
+ ## ------------------------------------------------------------
19
+ ## 1. Parse YAML using the same algorithm Rails::Application#config_for
20
+ ## uses (shared‑section merge, ERB, symbolize_keys, etc.)
21
+ ## ------------------------------------------------------------
22
+ merged =
23
+ if defined?(Rails::Application)
24
+ # Leverage the very method you pasted:
25
+ Rails.application.config_for(file, env: env).deep_dup
26
+ else
27
+ # Stand‑alone Ruby script: replicate the merge rules.
28
+ raw = ActiveSupport::ConfigurationFile.parse(file).deep_symbolize_keys
29
+ config = raw[env.to_sym] || {}
30
+ shared = raw[:shared] || {}
31
+ shared.deep_merge(config)
32
+ end
33
+
34
+ return merged.with_indifferent_access if name.to_s == '*'
35
+
36
+ merged.fetch(name.to_sym) do
37
+ raise KeyError,
38
+ "No '#{name}' connection in #{env} section of #{file}"
39
+ end.with_indifferent_access
40
+ end
41
+
42
+ def self.default_path
43
+ if defined?(Rails)
44
+ Rails.root.join('config', 'cypher_databases.yml')
45
+ else
46
+ File.join(Dir.pwd, 'config', 'cypher_databases.yml')
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators/base'
4
+
5
+ module ActiveCypher
6
+ module Generators
7
+ class InstallGenerator < Rails::Generators::Base
8
+ desc 'Creates cypher_databases.yml and an initializer for ActiveCypher'
9
+
10
+ source_root File.expand_path('templates', __dir__)
11
+
12
+ def copy_configuration
13
+ template 'cypher_databases.yml', 'config/cypher_databases.yml'
14
+ end
15
+
16
+ def copy_base_classes
17
+ empty_directory 'app/graph'
18
+ template 'application_graph_node.rb', 'app/graph/application_graph_node.rb'
19
+ template 'application_graph_relationship.rb', 'app/graph/application_graph_relationship.rb'
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators/active_model'
4
+ require 'rails/generators/named_base'
5
+
6
+ module ActiveCypher
7
+ module Generators
8
+ class NodeGenerator < Rails::Generators::NamedBase
9
+ source_root File.expand_path('templates', __dir__)
10
+
11
+ argument :attributes, type: :array,
12
+ default: [], banner: 'name:type name:type'
13
+
14
+ class_option :labels, type: :string,
15
+ desc: 'Comma‑separated Cypher labels',
16
+ default: ''
17
+
18
+ def create_node_file
19
+ template 'node.rb.erb',
20
+ File.join('app/graph', class_path, "#{file_name}.rb")
21
+ end
22
+
23
+ private
24
+
25
+ # helper for ERB
26
+ def labels_list
27
+ lbls = options[:labels].split(',').map(&:strip).reject(&:blank?)
28
+ lbls.empty? ? [class_name.gsub(/Node$/, '')] : lbls
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators/active_model'
4
+ require 'rails/generators/named_base'
5
+
6
+ module ActiveCypher
7
+ module Generators
8
+ class RelationshipGenerator < Rails::Generators::NamedBase
9
+ source_root File.expand_path('templates', __dir__)
10
+
11
+ argument :attributes, type: :array,
12
+ default: [], banner: 'name:type name:type'
13
+
14
+ class_option :from, type: :string, required: true,
15
+ desc: 'Source node class (e.g. UserNode)'
16
+ class_option :to, type: :string, required: true,
17
+ desc: 'Target node class (e.g. CompanyNode)'
18
+ class_option :type, type: :string, default: '',
19
+ desc: 'Cypher relationship type (defaults to class name)'
20
+
21
+ def create_relationship_file
22
+ template 'relationship.rb.erb',
23
+ File.join('app/graph', class_path, "#{file_name}.rb")
24
+ end
25
+
26
+ private
27
+
28
+ def relationship_type
29
+ (options[:type].presence || class_name).upcase
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ApplicationGraphNode < ActiveCypher::Base
4
+ # Adapter‑specific helpers are injected after connection
5
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ApplicationGraphRelationship < ActiveCypher::Relationship
4
+ # Relationship‑level helpers or callbacks go here
5
+ end
@@ -0,0 +1,28 @@
1
+ shared:
2
+ ssl: false # Because encryption is for people who deploy to the public internet on purpose
3
+
4
+ development:
5
+ primary: &local_memgraph
6
+ adapter: neo4j # Because you like your graphs with a touch of existential dread
7
+ uri: neo4j://localhost:7687 # Port 7687: also known as Neo4j’s VIP lounge
8
+ username: neo4j # Default username because who has time for originality
9
+ password: neo4j # Seriously. Change this. Right now. I can see you not doing it.
10
+ multi_db: false # One DB to rule them all, because multiple would involve effort
11
+
12
+ test:
13
+ primary:
14
+ <<: *local_memgraph # Reusing the dev config like a true copy-paste warrior
15
+ uri: bolt://localhost:9876 # Different port, same chaos
16
+
17
+ production:
18
+ primary:
19
+ adapter: memgraph # Yes, it’s still memgraph... for now...
20
+ uri: bolt+s://<%= ENV["MG_HOST"] %>:7687 # Now with SSL, because prod gets the good stuff
21
+ username: <%= ENV["MG_USER"] %> # You should probably not hardcode "admin"
22
+ password: <%= ENV["MG_PASS"] %> # Hopefully not "password123"
23
+ ssl: true # SSL: Because “oops, we got hacked” is a bad press release
24
+ multi_db: <%= ENV.fetch("MG_MULTI_DB", "false") %> # Default is "false" because complexity is scary and licensing is expensive
25
+
26
+ # Bonus content:
27
+ # Neo4j used to run prod but got demoted when Memgraph bought us lunch
28
+ # RIP Neo4j: gone but not forgotten... mostly because we still pay for the license
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+ class <%= class_name %> < ApplicationGraphNode
3
+ <% labels_list.each do |lbl| %>
4
+ label :<%= lbl %>
5
+ <% end %>
6
+
7
+ <% attributes.each do |attr| -%>
8
+ attribute :<%= attr.name %>, :<%= attr.type || "string" %>
9
+ <% end -%>
10
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ class <%= class_name %> < ApplicationGraphRelationship
4
+ from_class :<%= options[:from] %>
5
+ to_class :<%= options[:to] %>
6
+ type :<%= relationship_type %>
7
+
8
+ <% attributes.each do |attr| -%>
9
+ attribute :<%= attr.name %>, :<%= attr.type || "string" %>
10
+ <% end -%>
11
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+ require 'active_support/tagged_logging'
5
+
6
+ module ActiveCypher
7
+ module Logging
8
+ class << self
9
+ # The one true logger object
10
+ attr_accessor :backend
11
+
12
+ # Public accessor used by the mix‑in
13
+ def logger
14
+ self.backend ||= begin
15
+ base = Logger.new($stdout)
16
+ base.level = ENV.fetch('AC_LOG_LEVEL', 'info').upcase
17
+ .then do |lvl|
18
+ Logger.const_get(lvl)
19
+ rescue StandardError
20
+ Logger::INFO
21
+ end
22
+ ActiveSupport::TaggedLogging.new(base).tap { |l| l.tagged! 'ActiveCypher' }
23
+ end
24
+ end
25
+ end
26
+
27
+ # ------------------------------------------------------------------
28
+ # Instance helpers
29
+ # ------------------------------------------------------------------
30
+ def logger = Logging.logger
31
+ def log_debug(msg) = logger.debug { msg }
32
+ def log_info(msg) = logger.info { msg }
33
+ def log_warn(msg) = logger.warn { msg }
34
+ def log_error(msg) = logger.error { msg }
35
+
36
+ def log_bench(label)
37
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
38
+ yield
39
+ ensure
40
+ ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1_000).round(2)
41
+ logger.debug { "#{label} (#{ms} ms)" }
42
+ end
43
+ end
44
+ end