activecypher 0.6.2 → 0.7.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: 8d9c41285d4f1a84779d72a479b329e6c397d4630473d369691456e35f4337fc
4
- data.tar.gz: 1673192a6552da378dfd625114f8bc8eee8e973eb17583d802de8fcbcd609c70
3
+ metadata.gz: 001236241c04680c15946488d5f7e915100342dad161abe512f5e1d1f47bb84f
4
+ data.tar.gz: e6cbcd8f09b83bdfbb90830aded7a39ad7afc91c7aef03704d7dba8a55bcc12d
5
5
  SHA512:
6
- metadata.gz: 6feecb3dbad832f8f8253f64b3ccb030d2fe99194824d57bf5087db7caee2e335a4e7a6b21646e7ba098250812f3b05d4a5c812b7d8b0cc5b8e93e23f1c56ebb
7
- data.tar.gz: f389ab164a37e002523c55e050abc1bee7c20df6e5c6e11e311f2a5f6200a33d27ac4512f676f36eebd155ce2f71f21c87a08f9431f0fe641d1051f09aee21be
6
+ metadata.gz: 2e4a8e7f74da14f8013152a7a4adebfbc0cab73be88919a3ae14994f2fbeaab1599832659fd31b6b4f7a6c5f14de14439aad29e913b17981bc62b2f46fdf824d
7
+ data.tar.gz: b856e47065693eaab0152246a2aa7ee475706ef9c0b54c61d8e3746438971c670cb0157087d6f4456ee3e5757619e114d796880d5213d72c62048228007bc9ba
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'active_support/concern'
4
- require 'active_support/core_ext/string/inflections' # for camelize, singularize etc.
5
-
6
3
  module ActiveCypher
7
4
  # Module to handle association definitions (has_many, belongs_to, etc.)
8
5
  # for ActiveCypher models.
@@ -17,6 +17,26 @@ module ActiveCypher
17
17
  attr_reader :host, :port, :timeout_seconds, :socket,
18
18
  :protocol_version, :server_agent, :connection_id, :adapter
19
19
 
20
+ # Override inspect to redact sensitive information
21
+ def inspect
22
+ filtered_auth = ActiveCypher::Redaction.filter_hash(@auth_token)
23
+
24
+ attributes = {
25
+ host: @host.inspect,
26
+ port: @port.inspect,
27
+ auth_token: filtered_auth.inspect,
28
+ timeout_seconds: @timeout_seconds.inspect,
29
+ secure: @secure.inspect,
30
+ verify_cert: @verify_cert.inspect,
31
+ connected: @connected.inspect,
32
+ protocol_version: @protocol_version.inspect,
33
+ server_agent: @server_agent.inspect,
34
+ connection_id: @connection_id.inspect
35
+ }
36
+
37
+ "#<#{self.class.name}:0x#{object_id.to_s(16)} #{attributes.map { |k, v| "@#{k}=#{v}" }.join(', ')}>"
38
+ end
39
+
20
40
  SUPPORTED_VERSIONS = [5.8, 5.2].freeze
21
41
 
22
42
  # Initializes a new Bolt connection.
@@ -82,6 +82,15 @@ module ActiveCypher
82
82
  # @return [Array<Hash>] The processed rows
83
83
  def process_records(rows) = rows.map { |r| deep_symbolize(r) }
84
84
 
85
+ # Override inspect to hide sensitive information
86
+ # @return [String] Safe representation of the adapter
87
+ def inspect
88
+ filtered_config = ActiveCypher::Redaction.filter_hash(config)
89
+
90
+ # Return a safe representation
91
+ "#<#{self.class}:0x#{object_id.to_s(16)} @config=#{filtered_config.inspect}>"
92
+ end
93
+
85
94
  private
86
95
 
87
96
  # Recursively turns everything into symbols, because that's what all the cool kids do.
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+ require "active_cypher/schema/catalog"
2
3
 
3
4
  module ActiveCypher
4
5
  module ConnectionAdapters
@@ -6,6 +7,15 @@ module ActiveCypher
6
7
  # Register this adapter with the registry
7
8
  Registry.register('memgraph', self)
8
9
 
10
+ def vendor = :memgraph
11
+
12
+ def schema_catalog
13
+ rows = run('SHOW SCHEMA')
14
+ parse_schema(rows)
15
+ rescue StandardError
16
+ introspect_fallback
17
+ end
18
+
9
19
  # Use id() for Memgraph instead of elementId()
10
20
  ID_FUNCTION = 'id'
11
21
 
@@ -58,6 +68,36 @@ module ActiveCypher
58
68
 
59
69
  protected
60
70
 
71
+ def parse_schema(rows)
72
+ nodes, edges, idx, cons = [], [], [], []
73
+
74
+ rows.each do |row|
75
+ case row['type']
76
+ when 'NODE'
77
+ nodes << Schema::NodeTypeDef.new(row['label'], row['properties'], row['primaryKey'])
78
+ when 'EDGE'
79
+ edges << Schema::EdgeTypeDef.new(row['label'], row['from'], row['to'], row['properties'])
80
+ when 'INDEX'
81
+ idx << Schema::IndexDef.new(row['name'], :node, row['label'], row['properties'], row['unique'], nil)
82
+ when 'CONSTRAINT'
83
+ cons << Schema::ConstraintDef.new(row['name'], row['label'], row['properties'], :unique)
84
+ end
85
+ end
86
+
87
+ Schema::Catalog.new(indexes: idx, constraints: cons, node_types: nodes, edge_types: edges)
88
+ end
89
+
90
+ def introspect_fallback
91
+ labels = run('MATCH (n) RETURN DISTINCT labels(n) AS lbl').flat_map { |r| r['lbl'] }
92
+
93
+ nodes = labels.map do |lbl|
94
+ props = run("MATCH (n:`#{lbl}`) WITH n LIMIT 100 UNWIND keys(n) AS k RETURN DISTINCT k").map { |r| r['k'] }
95
+ Schema::NodeTypeDef.new(lbl, props, nil)
96
+ end
97
+
98
+ Schema::Catalog.new(indexes: [], constraints: [], node_types: nodes, edge_types: [])
99
+ end
100
+
61
101
  def protocol_handler_class = ProtocolHandler
62
102
 
63
103
  def validate_connection
@@ -1,10 +1,43 @@
1
1
  # frozen_string_literal: true
2
+ require "active_cypher/schema/catalog"
2
3
 
3
4
  module ActiveCypher
4
5
  module ConnectionAdapters
5
6
  class Neo4jAdapter < AbstractBoltAdapter
6
7
  Registry.register('neo4j', self)
7
8
 
9
+ def vendor = :neo4j
10
+
11
+ def schema_catalog
12
+ idx_rows = run('SHOW INDEXES')
13
+ con_rows = run('SHOW CONSTRAINTS')
14
+
15
+ idx_defs = idx_rows.map do |r|
16
+ Schema::IndexDef.new(
17
+ r['name'],
18
+ r['entityType'].downcase.to_sym,
19
+ r['labelsOrTypes'].first,
20
+ r['properties'],
21
+ r['uniqueness'] == 'UNIQUE',
22
+ r['type'] == 'VECTOR' ? r['options'] : nil
23
+ )
24
+ end
25
+
26
+ con_defs = con_rows.map do |r|
27
+ Schema::ConstraintDef.new(
28
+ r['name'],
29
+ r['labelsOrTypes'].first,
30
+ r['properties'],
31
+ r['type'].split('_').first.downcase.to_sym
32
+ )
33
+ end
34
+
35
+ Schema::Catalog.new(indexes: idx_defs, constraints: con_defs,
36
+ node_types: [], edge_types: [])
37
+ rescue StandardError
38
+ Schema::Catalog.new(indexes: [], constraints: [], node_types: [], edge_types: [])
39
+ end
40
+
8
41
  # Use elementId() for Neo4j
9
42
  ID_FUNCTION = 'elementId'
10
43
 
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators/named_base'
4
+
5
+ module ActiveCypher
6
+ module Generators
7
+ class MigrationGenerator < Rails::Generators::NamedBase
8
+ source_root File.expand_path('templates', __dir__)
9
+
10
+ def create_migration_file
11
+ timestamp = Time.now.utc.strftime('%Y%m%d%H%M%S')
12
+ dir = File.join('graphdb', 'migrate')
13
+ FileUtils.mkdir_p(dir)
14
+ template 'migration.rb.erb', File.join(dir, "#{timestamp}_#{file_name}.rb")
15
+ end
16
+ end
17
+ end
18
+ end
@@ -1,16 +1,11 @@
1
1
  development:
2
2
  primary:
3
- adapter: neo4j # Because you like your graphs with a touch of existential dread
4
- url: neo4j://neo4j:neo4j@localhost:7687 # VIP port, VIP credentials (change them, seriously)
5
- multi_db: false # One DB to rule them all
3
+ url: ENV["GRAPH_URL"]
6
4
 
7
5
  test:
8
6
  primary:
9
- url: neo4j://neo4j:neo4j@localhost:9876 # Different port, same chaos
10
- multi_db: false
7
+ url: ENV["GRAPH_URL"]
11
8
 
12
9
  production:
13
10
  primary:
14
- adapter: memgraph # Yes, still memgraph... for now...
15
- url: memgraph+ssc://<%= ENV["MG_USER"] %>:<%= ENV["MG_PASS"] %>@<%= ENV["MG_HOST"] %>:7687
16
- multi_db: <%= ENV.fetch("MG_MULTI_DB", "false") %> # Because complexity is a luxury
11
+ url: ENV["GRAPH_URL"]
@@ -0,0 +1,5 @@
1
+ class <%= name.camelize %> < ActiveCypher::Migration
2
+ up do
3
+ # add operations or incantation here
4
+ end
5
+ end
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class <%= class_name %> < ApplicationGraphRelationship
4
- from_class :<%= options[:from] %>
5
- to_class :<%= options[:to] %>
6
- type :<%= relationship_type %>
4
+ from_class '<%= options[:from] %>'
5
+ to_class '<%= options[:to] %>'
6
+ type '<%= relationship_type %>'
7
7
  <% if attributes.any? -%>
8
8
  <% attributes.each do |attr| -%>
9
9
  attribute :<%= attr.name %>, :<%= attr.type || "string" %>
@@ -132,7 +132,7 @@ module ActiveCypher
132
132
  # @param key [String, Symbol] The key to check
133
133
  # @return [Boolean] True if the key contains sensitive information
134
134
  def sensitive_key?(key)
135
- return true if key.to_s.match?(/\b(password|token|secret|credential|key)\b/i)
135
+ return true if key.to_s.match?(/(^|[\-_])(?:password|token|secret|credential|key)($|[\-_])/i)
136
136
 
137
137
  # Check against Rails filter parameters if available
138
138
  if defined?(Rails) && Rails.application
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveCypher
4
+ # Base class for GraphDB migrations.
5
+ # Provides a small DSL for defining index and constraint operations.
6
+ class Migration
7
+ class << self
8
+ attr_reader :up_block
9
+
10
+ # Define the migration steps.
11
+ def up(&block)
12
+ @up_block = block if block_given?
13
+ end
14
+ end
15
+
16
+ attr_reader :connection, :operations
17
+
18
+ def initialize(connection = ActiveCypher::Base.connection)
19
+ @connection = connection
20
+ @operations = []
21
+ end
22
+
23
+ # Execute the migration.
24
+ def run
25
+ instance_eval(&self.class.up_block) if self.class.up_block
26
+ execute_operations
27
+ end
28
+
29
+ # DSL ---------------------------------------------------------------
30
+
31
+ def create_node_index(label, *props, unique: false, if_not_exists: true, name: nil)
32
+ props_clause = props.map { |p| "n.#{p}" }.join(', ')
33
+ cypher = +'CREATE '
34
+ cypher << 'UNIQUE ' if unique
35
+ cypher << 'INDEX'
36
+ cypher << " #{name}" if name
37
+ cypher << ' IF NOT EXISTS' if if_not_exists
38
+ cypher << " FOR (n:#{label}) ON (#{props_clause})"
39
+ operations << cypher
40
+ end
41
+
42
+ def create_rel_index(rel_type, *props, if_not_exists: true, name: nil)
43
+ props_clause = props.map { |p| "r.#{p}" }.join(', ')
44
+ cypher = +'CREATE INDEX'
45
+ cypher << " #{name}" if name
46
+ cypher << ' IF NOT EXISTS' if if_not_exists
47
+ cypher << " FOR ()-[r:#{rel_type}]-() ON (#{props_clause})"
48
+ operations << cypher
49
+ end
50
+
51
+ def create_uniqueness_constraint(label, *props, if_not_exists: true, name: nil)
52
+ props_clause = props.map { |p| "n.#{p}" }.join(', ')
53
+ cypher = +'CREATE CONSTRAINT'
54
+ cypher << " #{name}" if name
55
+ cypher << ' IF NOT EXISTS' if if_not_exists
56
+ cypher << " FOR (n:#{label}) REQUIRE (#{props_clause}) IS UNIQUE"
57
+ operations << cypher
58
+ end
59
+
60
+ def execute(cypher_string)
61
+ operations << cypher_string.strip
62
+ end
63
+
64
+ private
65
+
66
+ def execute_operations
67
+ tx = connection.begin_transaction if connection.respond_to?(:begin_transaction)
68
+ operations.each do |cypher|
69
+ if tx
70
+ tx.run(cypher)
71
+ else
72
+ connection.execute_cypher(cypher)
73
+ end
74
+ end
75
+ connection.commit_transaction(tx) if tx
76
+ rescue StandardError
77
+ connection.rollback_transaction(tx) if tx
78
+ raise
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveCypher
4
+ # Runs pending graph database migrations.
5
+ class Migrator
6
+ MIGRATE_DIR = File.join('graphdb', 'migrate')
7
+
8
+ def initialize(connection = ActiveCypher::Base.connection)
9
+ @connection = connection
10
+ end
11
+
12
+ def migrate!
13
+ ensure_schema_migration_constraint
14
+ applied = existing_versions
15
+
16
+ migration_files.each do |file|
17
+ version = File.basename(file)[0, 14]
18
+ next if applied.include?(version)
19
+
20
+ require file
21
+ class_name = File.basename(file, '.rb').split('_', 2).last.camelize
22
+ klass = Object.const_get(class_name)
23
+ klass.new(@connection).run
24
+
25
+ @connection.execute_cypher(<<~CYPHER)
26
+ CREATE (:SchemaMigration { version: '#{version}', executed_at: datetime() })
27
+ CYPHER
28
+ end
29
+ end
30
+
31
+ def status
32
+ ensure_schema_migration_constraint
33
+ applied = existing_versions
34
+ migration_files.map do |file|
35
+ version = File.basename(file)[0, 14]
36
+ {
37
+ status: (applied.include?(version) ? 'up' : 'down'),
38
+ version: version,
39
+ name: File.basename(file)
40
+ }
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def adapter_dir
47
+ name = @connection.class.name.demodulize.sub('Adapter', '').downcase
48
+ File.join('graphdb', name)
49
+ end
50
+
51
+ def migration_dirs
52
+ dirs = [MIGRATE_DIR, adapter_dir]
53
+ extra = @connection.config[:migrations_paths]
54
+ dirs.concat(Array(extra)) if extra
55
+ dirs
56
+ end
57
+
58
+ def migration_files
59
+ migration_dirs.flat_map do |dir|
60
+ Dir[File.expand_path(File.join(dir, '*.rb'), Dir.pwd)]
61
+ end.sort
62
+ end
63
+
64
+ def existing_versions
65
+ @connection.execute_cypher('MATCH (m:SchemaMigration) RETURN m.version AS version')
66
+ .map { |r| r[:version].to_s }
67
+ rescue StandardError
68
+ []
69
+ end
70
+
71
+ def ensure_schema_migration_constraint
72
+ @connection.execute_cypher(<<~CYPHER)
73
+ CREATE CONSTRAINT graph_schema_migration IF NOT EXISTS
74
+ FOR (m:SchemaMigration)
75
+ REQUIRE m.version IS UNIQUE
76
+ CYPHER
77
+ end
78
+ end
79
+ end
@@ -64,6 +64,12 @@ module ActiveCypher
64
64
  require 'active_cypher/generators/install_generator'
65
65
  require 'active_cypher/generators/node_generator'
66
66
  require 'active_cypher/generators/relationship_generator'
67
+ require 'active_cypher/generators/migration_generator'
68
+ end
69
+
70
+ rake_tasks do
71
+ load File.expand_path('../tasks/graphdb_migrate.rake', __dir__)
72
+ load File.expand_path('../tasks/graphdb_schema.rake', __dir__)
67
73
  end
68
74
  end
69
75
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveCypher
4
+ # Shared constants and utilities for redacting sensitive information in inspection output
5
+ module Redaction
6
+ # The mask to use for sensitive information
7
+ MASK = '[HUNTER2]'
8
+
9
+ # Common sensitive parameter keys
10
+ SENSITIVE_KEYS = %i[password credentials auth_token principal url].freeze
11
+
12
+ # Create a parameter filter with the default mask and keys
13
+ # @param additional_keys [Array<Symbol>] Additional keys to redact
14
+ # @return [ActiveSupport::ParameterFilter] The configured filter
15
+ def self.create_filter(additional_keys = [])
16
+ keys = SENSITIVE_KEYS + additional_keys
17
+ ActiveSupport::ParameterFilter.new(keys, mask: MASK)
18
+ end
19
+
20
+ # Filter a hash to redact sensitive information
21
+ # @param hash [Hash] The hash to filter
22
+ # @param additional_keys [Array<Symbol>] Additional keys to redact
23
+ # @return [Hash] The filtered hash
24
+ def self.filter_hash(hash, additional_keys = [])
25
+ create_filter(additional_keys).filter(hash)
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,14 @@
1
+ module ActiveCypher
2
+ module Schema
3
+ IndexDef = Data.define(:name, :element, :label, :props, :unique, :vector_opts)
4
+ ConstraintDef = Data.define(:name, :label, :props, :kind)
5
+ NodeTypeDef = Data.define(:label, :props, :primary_key)
6
+ EdgeTypeDef = Data.define(:type, :from, :to, :props)
7
+
8
+ Catalog = Data.define(:indexes, :constraints, :node_types, :edge_types) do
9
+ def empty?
10
+ indexes.empty? && constraints.empty? && node_types.empty? && edge_types.empty?
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,96 @@
1
+ require 'fileutils'
2
+ require 'optparse'
3
+
4
+ module ActiveCypher
5
+ module Schema
6
+ # Dumps the graph schema to a Cypher script
7
+ class Dumper
8
+ DEFAULT_PATH = 'graphdb'.freeze
9
+
10
+ def initialize(connection = ActiveCypher::Base.connection, base_dir: Dir.pwd)
11
+ @connection = connection
12
+ @base_dir = base_dir
13
+ end
14
+
15
+ def dump_to_string
16
+ cat = @connection.schema_catalog
17
+ cat = catalog_from_migrations if cat.respond_to?(:empty?) && cat.empty?
18
+ Writer::Cypher.new(cat, @connection.vendor).to_s
19
+ end
20
+
21
+ def dump_to_file(path)
22
+ FileUtils.mkdir_p(File.dirname(path))
23
+ File.write(path, dump_to_string)
24
+ path
25
+ end
26
+
27
+ def self.run_from_cli(argv = ARGV)
28
+ opts = { stdout: false, connection: :primary }
29
+ OptionParser.new do |o|
30
+ o.on('--stdout') { opts[:stdout] = true }
31
+ o.on('--connection=NAME') { |v| opts[:connection] = v.to_sym }
32
+ end.parse!(argv)
33
+
34
+ pool = ActiveCypher::Base.connection_handler.pool(opts[:connection])
35
+ raise "Unknown connection: #{opts[:connection]}" unless pool
36
+
37
+ dumper = new(pool.connection)
38
+ file = output_file(opts[:connection])
39
+ if opts[:stdout]
40
+ puts dumper.dump_to_string
41
+ else
42
+ dumper.dump_to_file(file)
43
+ puts "Written #{file}"
44
+ end
45
+ end
46
+
47
+ def self.output_file(conn)
48
+ case conn.to_sym
49
+ when :primary
50
+ File.join(DEFAULT_PATH, 'schema.cypher')
51
+ when :analytics
52
+ File.join(DEFAULT_PATH, 'schema.analytics.cypher')
53
+ else
54
+ File.join(DEFAULT_PATH, "schema.#{conn}.cypher")
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ def catalog_from_migrations
61
+ idx = []
62
+ cons = []
63
+ Dir[File.join(@base_dir, 'graphdb', 'migrate', '*.rb')].sort.each do |file|
64
+ require file
65
+ class_name = File.basename(file, '.rb').split('_', 2).last.camelize
66
+ klass = Object.const_get(class_name)
67
+ mig = klass.new(@connection)
68
+ mig.instance_eval(&klass.up_block) if klass.respond_to?(:up_block) && klass.up_block
69
+ mig.operations.each do |cy|
70
+ if cy =~ /CREATE\s+(UNIQUE\s+)?INDEX/i
71
+ unique = !Regexp.last_match(1).nil?
72
+ name = cy[/CREATE\s+(?:UNIQUE\s+)?INDEX\s+(\w+)/i, 1] || 'idx'
73
+ label = cy[/\(n:`?([^:`)]+)`?\)/, 1] || 'Unknown'
74
+ props = cy[/ON \(([^)]+)\)/i, 1].to_s.split(',').map { |p| p.strip.sub(/^n\./, '').sub(/^r\./, '') }
75
+ elem = cy.include?('-[r:') ? :relationship : :node
76
+ idx << IndexDef.new(name, elem, label, props, unique, nil)
77
+ elsif cy =~ /CREATE\s+CONSTRAINT/i
78
+ name = cy[/CREATE\s+CONSTRAINT\s+(\w+)/i, 1] || 'constraint'
79
+ label = cy[/\(n:`?([^:`)]+)`?\)/, 1] || 'Unknown'
80
+ if cy =~ /UNIQUE/i
81
+ props = cy[/\(([^)]+)\)/, 1].to_s.split(',').map { |p| p.strip.sub(/^n\./, '') }
82
+ kind = :unique
83
+ else
84
+ prop = cy[/n\.(\w+)\s+IS NOT NULL/i, 1]
85
+ props = [prop].compact
86
+ kind = :exists
87
+ end
88
+ cons << ConstraintDef.new(name, label, props, kind)
89
+ end
90
+ end
91
+ end
92
+ Catalog.new(indexes: idx, constraints: cons, node_types: [], edge_types: [])
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,75 @@
1
+ module ActiveCypher
2
+ module Schema
3
+ module Writer
4
+ class Cypher
5
+ def initialize(catalog, vendor)
6
+ @catalog = catalog
7
+ @vendor = vendor
8
+ end
9
+
10
+ def to_s
11
+ sections = []
12
+ sections << constraint_lines(@catalog.constraints)
13
+ sections << index_lines(@catalog.indexes.select { |i| i.element == :node }, :node)
14
+ sections << index_lines(@catalog.indexes.select { |i| i.element == :relationship }, :relationship)
15
+ if @vendor == :memgraph
16
+ sections << node_type_lines(@catalog.node_types)
17
+ sections << edge_type_lines(@catalog.edge_types)
18
+ end
19
+ sections.reject(&:empty?).join("\n")
20
+ end
21
+
22
+ private
23
+
24
+ def constraint_lines(list)
25
+ list.sort_by(&:name).map do |c|
26
+ props = c.props.map { |p| "n.#{p}" }.join(', ')
27
+ case c.kind
28
+ when :unique
29
+ "CREATE CONSTRAINT #{c.name} FOR (n:`#{c.label}`) REQUIRE (#{props}) IS UNIQUE"
30
+ when :exists
31
+ "CREATE CONSTRAINT #{c.name} FOR (n:`#{c.label}`) REQUIRE n.#{c.props.first} IS NOT NULL"
32
+ else
33
+ "-- UNKNOWN CONSTRAINT #{c.name}"
34
+ end
35
+ end.join("\n")
36
+ end
37
+
38
+ def index_lines(list, element)
39
+ list.sort_by(&:name).map do |i|
40
+ if @vendor == :memgraph && i.vector_opts
41
+ "-- NOT-SUPPORTED ON MEMGRAPH 3.2: Vector index #{i.name}"
42
+ else
43
+ var = element == :node ? 'n' : 'r'
44
+ target = element == :node ? "(#{var}:`#{i.label}`)" : "()-[#{var}:`#{i.label}`]-()"
45
+ props = i.props.map { |p| "#{var}.#{p}" }.join(', ')
46
+ line = +'CREATE '
47
+ line << 'UNIQUE ' if i.unique
48
+ line << "INDEX #{i.name} FOR #{target} ON (#{props})"
49
+ if i.vector_opts && @vendor == :neo4j
50
+ opts = i.vector_opts.map { |k, v| "#{k}: #{v}" }.join(', ')
51
+ line << " OPTIONS { #{opts} }"
52
+ end
53
+ line
54
+ end
55
+ end.join("\n")
56
+ end
57
+
58
+ def node_type_lines(list)
59
+ list.sort_by(&:label).map do |nt|
60
+ props = nt.props.join(', ')
61
+ pk = nt.primary_key ? " PRIMARY KEY #{nt.primary_key}" : ''
62
+ "CREATE NODE TYPE #{nt.label}#{pk} PROPERTIES #{props}"
63
+ end.join("\n")
64
+ end
65
+
66
+ def edge_type_lines(list)
67
+ list.sort_by(&:type).map do |et|
68
+ props = et.props.join(', ')
69
+ "CREATE EDGE TYPE #{et.type} FROM #{et.from} TO #{et.to} PROPERTIES #{props}"
70
+ end.join("\n")
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -1,5 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveCypher
4
- VERSION = '0.6.2'
4
+ VERSION = '0.7.0'
5
+
6
+ def self.gem_version
7
+ Gem::Version.new VERSION
8
+ end
9
+
10
+ class << self
11
+ alias version gem_version
12
+ end
5
13
  end
data/lib/activecypher.rb CHANGED
@@ -4,6 +4,7 @@ require 'active_support'
4
4
  require 'zeitwerk'
5
5
  require_relative 'cyrel'
6
6
  require_relative 'active_cypher/version'
7
+ require_relative 'active_cypher/redaction'
7
8
 
8
9
  # ActiveCypher is a Ruby gem that provides an ActiveRecord-like interface for
9
10
  # interacting with Neo4j databases using Cypher queries.
data/lib/cyrel/node.rb CHANGED
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'active_support/core_ext/string/inflections'
4
-
5
3
  module Cyrel
6
4
  # The base class for building Cypher queries.
7
5
  class Node
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :graphdb do
4
+ desc 'Run graph database migrations'
5
+ task migrate: :environment do
6
+ ActiveCypher::Migrator.new.migrate!
7
+ puts 'GraphDB migrations complete'
8
+ end
9
+
10
+ desc 'Show graph database migration status'
11
+ task status: :environment do
12
+ ActiveCypher::Migrator.new.status.each do |m|
13
+ puts format('%-4s %s %s', m[:status], m[:version], m[:name])
14
+ end
15
+ end
16
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activecypher
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.2
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Abdelkader Boudih
@@ -132,15 +132,19 @@ files:
132
132
  - lib/active_cypher/fixtures/registry.rb
133
133
  - lib/active_cypher/fixtures/rel_builder.rb
134
134
  - lib/active_cypher/generators/install_generator.rb
135
+ - lib/active_cypher/generators/migration_generator.rb
135
136
  - lib/active_cypher/generators/node_generator.rb
136
137
  - lib/active_cypher/generators/relationship_generator.rb
137
138
  - lib/active_cypher/generators/templates/application_graph_node.rb
138
139
  - lib/active_cypher/generators/templates/application_graph_relationship.rb
139
140
  - lib/active_cypher/generators/templates/cypher_databases.yml
141
+ - lib/active_cypher/generators/templates/migration.rb.erb
140
142
  - lib/active_cypher/generators/templates/node.rb.erb
141
143
  - lib/active_cypher/generators/templates/relationship.rb.erb
142
144
  - lib/active_cypher/instrumentation.rb
143
145
  - lib/active_cypher/logging.rb
146
+ - lib/active_cypher/migration.rb
147
+ - lib/active_cypher/migrator.rb
144
148
  - lib/active_cypher/model/abstract.rb
145
149
  - lib/active_cypher/model/attributes.rb
146
150
  - lib/active_cypher/model/callbacks.rb
@@ -154,9 +158,13 @@ files:
154
158
  - lib/active_cypher/model/persistence.rb
155
159
  - lib/active_cypher/model/querying.rb
156
160
  - lib/active_cypher/railtie.rb
161
+ - lib/active_cypher/redaction.rb
157
162
  - lib/active_cypher/relation.rb
158
163
  - lib/active_cypher/relationship.rb
159
164
  - lib/active_cypher/runtime_registry.rb
165
+ - lib/active_cypher/schema/catalog.rb
166
+ - lib/active_cypher/schema/dumper.rb
167
+ - lib/active_cypher/schema/writer/cypher.rb
160
168
  - lib/active_cypher/scoping.rb
161
169
  - lib/active_cypher/utils/logger.rb
162
170
  - lib/active_cypher/version.rb
@@ -204,6 +212,7 @@ files:
204
212
  - lib/cyrel/return_only.rb
205
213
  - lib/cyrel/types/hash_type.rb
206
214
  - lib/cyrel/types/symbol_type.rb
215
+ - lib/tasks/graphdb_migrate.rake
207
216
  - sig/activecypher.rbs
208
217
  homepage: https://github.com/seuros/activecypher
209
218
  licenses: