rails_age 0.6.1 → 0.6.2

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: 053d21a51d2e91cb61ea1678d2d84c05ac4a1a84a643334c4993df9f6ca9c3c0
4
- data.tar.gz: 57f945d6055d9dca60414321e3f39dd09abfdb1a8f2a3a5fec0b4c5e3e9ee656
3
+ metadata.gz: '01018a3efe8d4cef46fce365b6fd442882d8bf4ff51e0ad3d3bdb235db80ebb7'
4
+ data.tar.gz: 69a20eb39f9609c6350463d6a391b7f08887079e51e56602a6357d914571ed8f
5
5
  SHA512:
6
- metadata.gz: 017c585f995749dad85f341b41a01d2f94b0cc03fac6240d63e4755e8ab0c7148969793fbe9c308f13c0ea94d35eee3fb231516eb775817a53bef003843d41e4
7
- data.tar.gz: 83760cc109544589665b7297776538f48f4ac0c14273455957e714ff6d9614e1a4e8d8ba862e39b8735a5f1e5a1f9c8ca5e429f1823aee8b98e38bad3dded2de
6
+ metadata.gz: cc2ea56dcb102213397ca5ffc19edc095341e51307b0500d0be7bc7595cb65f64e74c6490a678703f5faf903d0396010b63b8dd33e162ed218e038a4e95e05f5
7
+ data.tar.gz: 30e0472fb006bf883cb57a5c7406571d4fc4715a9d6d96e792dd5d3b4998e538eab74f809b5d384072d88e77ce2d6fb143cdb6a5c20fd044fd74673edd2bb13a
data/CHANGELOG.md CHANGED
@@ -22,22 +22,34 @@
22
22
  * paths support
23
23
  * select attributes support
24
24
 
25
+ ## VERSION 0.8.0 - 2024-xx-xx
26
+
27
+ breaking change?: namespaces (by default) will use their own schema? (add to database.yml & schema.rb ?)
28
+
29
+ - **AGE Schema override**
30
+
31
+ - **Multiple AGE Schema**
32
+
25
33
  ## VERSION 0.7.0 - 2024-xx-xx
26
34
 
27
35
  - **Age Path** - nodes and edges combined
28
36
  * add `rails generate apache_age:path_scaffold HasJob employee_role start_node:person end_node:company`
29
37
 
30
- ## VERSION 0.6.3 - 2024-xx-xx
31
-
32
- breaking change?: namespaces (by default) will use their own schema? (add to database.yml & schema.rb ?)
33
38
 
34
- - **AGE Schema override**
39
+ ## VERSION 0.6.3 - 2024-xx-xx
35
40
 
36
- - **multiple AGE Schema**
41
+ - **Query Sanitize**:
42
+ * reject attributes not defined in model
43
+ * sanitize strings using: id(find) = ?, 23 & find.first_name = ?, 'John'
37
44
 
38
- ## VERSION 0.6.2 - 2024-xx-xx
45
+ ## VERSION 0.6.2 - 2024-09-30
39
46
 
40
47
  - **Query Sanitize**
48
+ * hashes sanitized
49
+
50
+ - **TODO**:
51
+ * reject attributes not defined in model
52
+ * sanitize strings using: id(find) = ?, 23 & find.first_name = ?, 'John'
41
53
 
42
54
  ## VERSION 0.6.1 - 2024-09-29
43
55
 
data/README.md CHANGED
@@ -387,7 +387,7 @@ query =
387
387
  .execute
388
388
  ```
389
389
 
390
- or more generally:
390
+ or more generally (soon - not yet tested nor santized):
391
391
 
392
392
  ```ruby
393
393
  tihen =
@@ -25,18 +25,20 @@ module ApacheAge
25
25
 
26
26
  # Private stuff
27
27
 
28
+ # used? or dead code?
28
29
  def where_edge(attributes)
29
30
  where_attribs =
30
31
  attributes
31
- .compact
32
- .except(:end_id, :start_id, :end_node, :start_node)
33
- .map { |k, v| "find.#{k} = '#{v}'" }.join(' AND ')
32
+ .compact
33
+ .except(:end_id, :start_id, :end_node, :start_node)
34
+ .map { |k, v| ActiveRecord::Base.sanitize_sql(["find.#{k} = ?", v]) }
35
+ .join(' AND ')
34
36
  where_attribs = where_attribs.empty? ? nil : where_attribs
35
37
 
36
38
  end_id = attributes[:end_id] || attributes[:end_node]&.id
37
39
  start_id = attributes[:start_id] || attributes[:start_node]&.id
38
- where_end_id = end_id ? "id(end_node) = #{end_id}" : nil
39
- where_start_id = start_id ? "id(start_node) = #{start_id}" : nil
40
+ where_end_id = end_id ? ActiveRecord::Base.sanitize_sql(["id(end_node) = ?", end_id]) : nil
41
+ where_start_id = start_id ? ActiveRecord::Base.sanitize_sql(["id(start_node) = ?", start_id]) : nil
40
42
 
41
43
  where_clause = [where_attribs, where_start_id, where_end_id].compact.join(' AND ')
42
44
  return nil if where_clause.empty?
@@ -115,33 +117,41 @@ module ApacheAge
115
117
 
116
118
  end_id =
117
119
  if attributes[:end_id]
118
- end_id = attributes[:end_id]
120
+ attributes[:end_id]
119
121
  elsif attributes[:end_node].is_a?(Node)
120
- end_id = attributes[:end_node]&.id
122
+ attributes[:end_node]&.id
121
123
  end
122
- where_end_id = end_id ? "id(end_node) = #{end_id}" : nil
124
+ where_end_id = end_id ? ActiveRecord::Base.sanitize_sql(["id(end_node) = ?", end_id]) : nil
123
125
 
124
126
  start_id =
125
127
  if attributes[:start_id]
126
- start_id = attributes[:start_id]
128
+ attributes[:start_id]
127
129
  elsif attributes[:start_node].is_a?(Node)
128
- start_id = attributes[:start_node]&.id
130
+ attributes[:start_node]&.id
129
131
  end
130
- where_start_id = start_id ? "id(start_node) = #{start_id}" : nil
132
+ where_start_id = start_id ? ActiveRecord::Base.sanitize_sql(["id(start_node) = ?", start_id]) : nil
131
133
 
132
134
  where_end_attrs =
133
- attributes[:end_node].map { |k, v| "end_node.#{k} = '#{v}'" } if attributes[:end_node].is_a?(Hash)
135
+ if attributes[:end_node].is_a?(Hash)
136
+ attributes[:end_node].map { |k, v| ActiveRecord::Base.sanitize_sql(["end_node.#{k} = ?", v]) }
137
+ end
134
138
  where_start_attrs =
135
- attributes[:start_node].map { |k, v| "start_node.#{k} = '#{v}'" } if attributes[:start_node].is_a?(Hash)
139
+ if attributes[:start_node].is_a?(Hash)
140
+ attributes[:start_node].map { |k, v| ActiveRecord::Base.sanitize_sql(["start_node.#{k} = ?", v]) }
141
+ end
136
142
 
137
143
  [core_clauses, where_start_id, where_end_id, where_start_attrs, where_end_attrs]
138
144
  .flatten.compact.join(' AND ')
139
145
  end
140
146
 
147
+
141
148
  def build_core_where_clause(attributes)
142
149
  attributes
143
150
  .compact
144
- .map { |k, v| k == :id ? "id(find) = #{v}" : "find.#{k} = '#{v}'"}
151
+ .map do |k, v|
152
+ query_string = k == :id ? "id(find) = #{v}" : "find.#{k} = '#{v}'"
153
+ ActiveRecord::Base.sanitize_sql([query_string, v])
154
+ end
145
155
  .join(' AND ')
146
156
  end
147
157
  end
@@ -57,13 +57,14 @@ module ApacheAge
57
57
  def destroy
58
58
  match_clause = (age_type == 'vertex' ? "(done:#{age_label})" : "()-[done:#{age_label}]->()")
59
59
  delete_clause = (age_type == 'vertex' ? 'DETACH DELETE done' : 'DELETE done')
60
+ sanitized_id = ActiveRecord::Base.sanitize_sql(["id(done) = ?", id])
60
61
  cypher_sql =
61
62
  <<-SQL
62
63
  SELECT *
63
64
  FROM cypher('#{age_graph}', $$
64
65
  MATCH #{match_clause}
65
- WHERE id(done) = #{id}
66
- #{delete_clause}
66
+ WHERE #{sanitized_id}
67
+ #{delete_clause}
67
68
  return done
68
69
  $$) as (deleted agtype);
69
70
  SQL
@@ -99,7 +100,8 @@ module ApacheAge
99
100
  def properties_to_s
100
101
  string_values =
101
102
  age_properties.each_with_object([]) do |(key, val), array|
102
- array << "#{key}: '#{val}'"
103
+ sanitized_val = ActiveRecord::Base.sanitize_sql(["?", val])
104
+ array << "#{key}: #{sanitized_val}"
103
105
  end
104
106
  "{#{string_values.join(', ')}}"
105
107
  end
@@ -70,12 +70,25 @@ module ApacheAge
70
70
  def create_sql
71
71
  self.start_node = start_node.save unless start_node.persisted?
72
72
  self.end_node = end_node.save unless end_node.persisted?
73
+
74
+ start_node_age_label = ActiveRecord::Base.sanitize_sql(start_node.age_label)
75
+ end_node_age_label = ActiveRecord::Base.sanitize_sql(end_node.age_label)
76
+ sanitized_start_id = ActiveRecord::Base.sanitize_sql(["?", start_node.id])
77
+ sanitized_end_id = ActiveRecord::Base.sanitize_sql(["?", end_node.id])
78
+ # cant use sanitize_sql_like because it escapes the % and _ characters
79
+ # label_name = ActiveRecord::Base.sanitize_sql_like(age_label)
80
+
81
+ reject_keys = %i[id start_id end_id start_node end_node]
82
+ sanitized_properties =
83
+ self.to_h.reject { |k, _v| reject_keys.include?(k) }.reject { |_k, v| v.nil? }
84
+ .map { |k, v| "#{k}: #{ActiveRecord::Base.sanitize_sql(["?", v])}" }
85
+ .join(', ')
73
86
  <<-SQL
74
87
  SELECT *
75
88
  FROM cypher('#{age_graph}', $$
76
- MATCH (from_node:#{start_node.age_label}), (to_node:#{end_node.age_label})
77
- WHERE id(from_node) = #{start_node.id} and id(to_node) = #{end_node.id}
78
- CREATE (from_node)-[edge#{self}]->(to_node)
89
+ MATCH (from_node:#{start_node_age_label}), (to_node:#{end_node_age_label})
90
+ WHERE id(from_node) = #{sanitized_start_id} AND id(to_node) = #{sanitized_end_id}
91
+ CREATE (from_node)-[edge:#{age_label} {#{sanitized_properties}}]->(to_node)
79
92
  RETURN edge
80
93
  $$) as (edge agtype);
81
94
  SQL
@@ -84,14 +97,24 @@ module ApacheAge
84
97
  # So far just properties of string type with '' around them
85
98
  def update_sql
86
99
  alias_name = age_alias || age_label.downcase
87
- set_caluse =
88
- age_properties.map { |k, v| v ? "#{alias_name}.#{k} = '#{v}'" : "#{alias_name}.#{k} = NULL" }.join(', ')
100
+ set_clause =
101
+ age_properties.map do |k, v|
102
+ if v
103
+ sanitized_value = ActiveRecord::Base.sanitize_sql(["?", v])
104
+ "#{alias_name}.#{k} = #{sanitized_value}"
105
+ else
106
+ "#{alias_name}.#{k} = NULL"
107
+ end
108
+ end.join(', ')
109
+
110
+ sanitized_id = ActiveRecord::Base.sanitize_sql(["?", id])
111
+
89
112
  <<-SQL
90
113
  SELECT *
91
114
  FROM cypher('#{age_graph}', $$
92
115
  MATCH ()-[#{alias_name}:#{age_label}]->()
93
- WHERE id(#{alias_name}) = #{id}
94
- SET #{set_caluse}
116
+ WHERE id(#{alias_name}) = #{sanitized_id}
117
+ SET #{set_clause}
95
118
  RETURN #{alias_name}
96
119
  $$) as (#{age_label} agtype);
97
120
  SQL
@@ -3,14 +3,20 @@ module ApacheAge
3
3
  class Entity
4
4
  class << self
5
5
  def find_by(attributes)
6
- where_clause = attributes.map { |k, v| "find.#{k} = '#{v}'" }.join(' AND ')
6
+ where_clause =
7
+ attributes
8
+ .map do |k, v|
9
+ if k == :id
10
+ ActiveRecord::Base.sanitize_sql(["id(find) = ?", v])
11
+ else
12
+ ActiveRecord::Base.sanitize_sql(["find.#{k} = ?", v])
13
+ end
14
+ end
15
+ .join(' AND ')
7
16
  handle_find(where_clause)
8
17
  end
9
18
 
10
- def find(id)
11
- where_clause = "id(find) = #{id}"
12
- handle_find(where_clause)
13
- end
19
+ def find(id) = find_by(id: id)
14
20
 
15
21
  private
16
22
 
@@ -46,19 +52,24 @@ module ApacheAge
46
52
  json_data = JSON.parse(json_string)
47
53
 
48
54
  age_label = json_data['label']
49
- attribs = json_data.except('label', 'properties')
50
- .merge(json_data['properties'])
51
- .symbolize_keys
55
+ attribs =
56
+ json_data
57
+ .except('label', 'properties')
58
+ .merge(json_data['properties'])
59
+ .symbolize_keys
52
60
 
53
61
  "#{json_data['label'].gsub('__', '::')}".constantize.new(**attribs)
54
62
  end
55
63
 
56
64
  def find_sql(match_clause, where_clause)
65
+ sanitized_match_clause = ActiveRecord::Base.sanitize_sql(match_clause)
66
+ sanitized_where_clause = where_clause # Already sanitized in `find_by` or `find`
67
+
57
68
  <<-SQL
58
69
  SELECT *
59
70
  FROM cypher('#{age_graph}', $$
60
- MATCH #{match_clause}
61
- WHERE #{where_clause}
71
+ MATCH #{sanitized_match_clause}
72
+ WHERE #{sanitized_where_clause}
62
73
  RETURN find
63
74
  $$) as (found agtype);
64
75
  SQL
@@ -25,11 +25,21 @@ module ApacheAge
25
25
  # RETURN company
26
26
  # $$) as (Company agtype);
27
27
  def create_sql
28
+ # can't use sanitiye without a solution for '_' in the alias name & label
29
+ # alias_name = ActiveRecord::Base.sanitize_sql_like(age_alias || age_label.downcase)
30
+ # label_name = ActiveRecord::Base.sanitize_sql_like(age_label)
31
+
28
32
  alias_name = age_alias || age_label.downcase
29
- <<-SQL
33
+ sanitized_properties =
34
+ self
35
+ .to_h.reject { |k, v| k == :id }.reject { |k, v| v.nil? }
36
+ .map { |k, v| "#{k}: #{ActiveRecord::Base.sanitize_sql(["?", v])}" }
37
+ .join(', ')
38
+
39
+ <<~SQL.squish
30
40
  SELECT *
31
41
  FROM cypher('#{age_graph}', $$
32
- CREATE (#{alias_name}#{self})
42
+ CREATE (#{alias_name}:#{age_label} {#{sanitized_properties}})
33
43
  RETURN #{alias_name}
34
44
  $$) as (#{age_label} agtype);
35
45
  SQL
@@ -37,19 +47,29 @@ module ApacheAge
37
47
 
38
48
  # So far just properties of string type with '' around them
39
49
  def update_sql
40
- alias_name = age_alias || age_label.downcase
41
- set_caluse =
42
- age_properties.map { |k, v| v ? "#{alias_name}.#{k} = '#{v}'" : "#{alias_name}.#{k} = NULL" }.join(', ')
50
+ alias_name = ActiveRecord::Base.sanitize_sql_like(age_alias || age_label.downcase)
51
+ sanitized_set_clause = age_properties.map do |k, v|
52
+ if v
53
+ sanitized_value = ActiveRecord::Base.sanitize_sql(["?", v])
54
+ "#{alias_name}.#{k} = #{sanitized_value}"
55
+ else
56
+ "#{alias_name}.#{k} = NULL"
57
+ end
58
+ end.join(', ')
59
+
60
+ sanitized_id = ActiveRecord::Base.sanitize_sql(["?", id])
61
+
43
62
  <<-SQL
44
63
  SELECT *
45
64
  FROM cypher('#{age_graph}', $$
46
65
  MATCH (#{alias_name}:#{age_label})
47
- WHERE id(#{alias_name}) = #{id}
48
- SET #{set_caluse}
66
+ WHERE id(#{alias_name}) = #{sanitized_id}
67
+ SET #{sanitized_set_clause}
49
68
  RETURN #{alias_name}
50
69
  $$) as (#{age_label} agtype);
51
70
  SQL
52
71
  end
72
+
53
73
  end
54
74
  end
55
75
  end
@@ -41,7 +41,10 @@ module ApacheAge
41
41
  self
42
42
  end
43
43
 
44
- # need to handle string inputs too: ie: "id(find) = #{id}"
44
+ # TODO: need to handle string inputs too: instead of: \
45
+ # "id(find) = #{id}" & "find.name = #{name}"
46
+ # we can have: "id(find) = ?", id & "find.name = ?", name
47
+ # ActiveRecord::Base.sanitize_sql([query_string, v])
45
48
  def where(attributes)
46
49
  return self if attributes.blank?
47
50
 
@@ -112,21 +115,23 @@ module ApacheAge
112
115
 
113
116
  private
114
117
 
118
+ # TODO: ensure ordering keys are present in the model
115
119
  def parse_ordering(ordering)
116
120
  if ordering.is_a?(Hash)
117
- # Convert hash into "find.key direction" format and join with commas
118
- ordering = ordering.map { |k, v| "find.#{k} #{v}" }.join(', ')
121
+ ordering =
122
+ ordering
123
+ .map { |k, v| "find.#{k} #{ActiveRecord::Base.sanitize_sql_like(v.to_s)}" }
124
+ .join(', ')
119
125
  elsif ordering.is_a?(Symbol)
120
- # If it's a symbol, simply prepend "find."
121
126
  ordering = "find.#{ordering}"
122
127
  elsif ordering.is_a?(String)
123
- # If it's a string, assume it's already in the correct format
124
- ordering = ordering
128
+ ordering
125
129
  elsif ordering.is_a?(Array)
126
- # If it's an array, process each element recursively
127
130
  ordering = ordering.map do |order|
128
131
  if order.is_a?(Hash)
129
- order.map { |k, v| "find.#{k} #{v}" }.join(', ')
132
+ order
133
+ .map { |k, v| "find.#{k} #{ActiveRecord::Base.sanitize_sql_like(v.to_s)}" }
134
+ .join(', ')
130
135
  elsif order.is_a?(Symbol)
131
136
  "find.#{order}"
132
137
  elsif order.is_a?(String)
@@ -154,6 +159,22 @@ module ApacheAge
154
159
  $$) AS (#{return_names.join(' agtype, ')} agtype);
155
160
  SQL
156
161
  end
162
+ # def build_query(_extra_clause = nil)
163
+ # sanitized_where_sql = where_clauses.any? ? "WHERE #{where_clauses.map { |clause| ActiveRecord::Base.sanitize_sql_like(clause) }.join(' AND ')}" : ''
164
+ # sanitized_order_by = order_clause.present? ? ActiveRecord::Base.sanitize_sql_like(order_clause) : ''
165
+ # sanitized_limit_clause = limit_clause.present? ? ActiveRecord::Base.sanitize_sql_like(limit_clause) : ''
166
+
167
+ # <<-SQL.squish
168
+ # SELECT *
169
+ # FROM cypher('#{graph_name}', $$
170
+ # MATCH #{ActiveRecord::Base.sanitize_sql_like(match_clause)}
171
+ # #{sanitized_where_sql}
172
+ # RETURN #{ActiveRecord::Base.sanitize_sql_like(return_clause)}
173
+ # #{sanitized_order_by}
174
+ # #{sanitized_limit_clause}
175
+ # $$) AS (#{return_names.map { |name| "#{ActiveRecord::Base.sanitize_sql_like(name)} agtype" }.join(', ')});
176
+ # SQL
177
+ # end
157
178
  end
158
179
  end
159
180
  end
@@ -1,3 +1,3 @@
1
1
  module RailsAge
2
- VERSION = '0.6.1'
2
+ VERSION = '0.6.2'
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_age
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.1
4
+ version: 0.6.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bill Tihen
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-09-29 00:00:00.000000000 Z
11
+ date: 2024-09-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -119,7 +119,6 @@ files:
119
119
  - lib/apache_age/entities/node.rb
120
120
  - lib/apache_age/entities/path.rb
121
121
  - lib/apache_age/entities/query_builder.rb
122
- - lib/apache_age/entities/vertex.rb
123
122
  - lib/apache_age/node.rb
124
123
  - lib/apache_age/path.rb
125
124
  - lib/apache_age/types/factory.rb
@@ -1,53 +0,0 @@
1
- # module ApacheAge
2
- # module Entities
3
- # module Vertex
4
- # extend ActiveSupport::Concern
5
-
6
- # included do
7
- # include ActiveModel::Model
8
- # include ActiveModel::Dirty
9
- # include ActiveModel::Attributes
10
-
11
- # attribute :id, :integer
12
-
13
- # extend ApacheAge::Entities::ClassMethods
14
- # include ApacheAge::Entities::CommonMethods
15
- # end
16
-
17
- # def age_type = 'vertex'
18
-
19
- # # AgeSchema::Nodes::Company.create(company_name: 'Bedrock Quarry')
20
- # # SELECT *
21
- # # FROM cypher('age_schema', $$
22
- # # CREATE (company:Company {company_name: 'Bedrock Quarry'})
23
- # # RETURN company
24
- # # $$) as (Company agtype);
25
- # def create_sql
26
- # alias_name = age_alias || age_label.downcase
27
- # <<-SQL
28
- # SELECT *
29
- # FROM cypher('#{age_graph}', $$
30
- # CREATE (#{alias_name}#{self})
31
- # RETURN #{alias_name}
32
- # $$) as (#{age_label} agtype);
33
- # SQL
34
- # end
35
-
36
- # # So far just properties of string type with '' around them
37
- # def update_sql
38
- # alias_name = age_alias || age_label.downcase
39
- # set_caluse =
40
- # age_properties.map { |k, v| v ? "#{alias_name}.#{k} = '#{v}'" : "#{alias_name}.#{k} = NULL" }.join(', ')
41
- # <<-SQL
42
- # SELECT *
43
- # FROM cypher('#{age_graph}', $$
44
- # MATCH (#{alias_name}:#{age_label})
45
- # WHERE id(#{alias_name}) = #{id}
46
- # SET #{set_caluse}
47
- # RETURN #{alias_name}
48
- # $$) as (#{age_label} agtype);
49
- # SQL
50
- # end
51
- # end
52
- # end
53
- # end