rails_age 0.6.0 → 0.6.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6777870b945c8684ea2ef5d10b8e7411259e002001b074754840a9422b594cee
4
- data.tar.gz: '008ab62bbe28d408d390acd5e00cecc764507e21ae3d38ca94d219e9d299435f'
3
+ metadata.gz: '01018a3efe8d4cef46fce365b6fd442882d8bf4ff51e0ad3d3bdb235db80ebb7'
4
+ data.tar.gz: 69a20eb39f9609c6350463d6a391b7f08887079e51e56602a6357d914571ed8f
5
5
  SHA512:
6
- metadata.gz: 8a9c122ce1a8b5b173fc9ffd6ba45a4bf97965eb0681ec0d44e9779dcc1ef647da8e7e5110a5564a9f28c329ebe9e83ba832d920f4504bb5026650f41cc102d2
7
- data.tar.gz: f9217b16e0177746a438ab5fed8a4e5c7f4bb6ff02b3e31a2a4d1db7a8bd246550e33be6494e0533c70dcbcde3a54078ed041b2c3a08718cb9b020c73353eee5
6
+ metadata.gz: cc2ea56dcb102213397ca5ffc19edc095341e51307b0500d0be7bc7595cb65f64e74c6490a678703f5faf903d0396010b63b8dd33e162ed218e038a4e95e05f5
7
+ data.tar.gz: 30e0472fb006bf883cb57a5c7406571d4fc4715a9d6d96e792dd5d3b4998e538eab74f809b5d384072d88e77ce2d6fb143cdb6a5c20fd044fd74673edd2bb13a
data/CHANGELOG.md CHANGED
@@ -1,6 +1,20 @@
1
1
  # Change Log
2
2
 
3
- ## VERSION 0.6.4 - 2024-xx-xx
3
+ ## VERSION 0.9.1 - xxxx-xx-xx
4
+
5
+ - **Edge Generator**
6
+ * add start-/end-nodes types to edge generator (would make scaffold easier), ie:
7
+ `rails generate apache_age:edge HasPet owner_role start_node:person end_node:pet`
8
+ with property and specified start-/end-nodes (person and pet nodes must have already been created)
9
+
10
+ - **Edge Scaffold** with node types?
11
+ * add `rails generate apache_age:edge_scaffold HasJob employee_role start_node:person end_node:company`
12
+
13
+ ## VERSION 0.9.0 - 2024-xx-xx
14
+ - **AGE visual paths graph**
15
+ * add `rails generate apache_age:visualize`
16
+
17
+ ## VERSION 0.8.0 - 2024-xx-xx
4
18
 
5
19
  - **cypher queries** (like active record queries)
6
20
  * schema override
@@ -8,37 +22,51 @@
8
22
  * paths support
9
23
  * select attributes support
10
24
 
11
- ## VERSION 0.6.3 - 2024-xx-xx
12
-
13
- - **Age Path**
14
-
15
- ## VERSION 0.6.2 - 2024-xx-xx
25
+ ## VERSION 0.8.0 - 2024-xx-xx
16
26
 
17
27
  breaking change?: namespaces (by default) will use their own schema? (add to database.yml & schema.rb ?)
18
28
 
19
29
  - **AGE Schema override**
20
30
 
21
- - **multiple AGE Schema**
31
+ - **Multiple AGE Schema**
22
32
 
23
- ## VERSION 0.6.1 - 2024-xx-xx
33
+ ## VERSION 0.7.0 - 2024-xx-xx
24
34
 
25
- - **Fix**
26
- * show validation errors in scaffold views
35
+ - **Age Path** - nodes and edges combined
36
+ * add `rails generate apache_age:path_scaffold HasJob employee_role start_node:person end_node:company`
27
37
 
28
- - **Edge Generator**
29
- * add start-/end-nodes types to edge generator (would make scaffold easier), ie:
30
- `rails generate apache_age:edge HasPet owner_role start_node:person end_node:pet`
31
- with property and specified start-/end-nodes (person and pet nodes must have already been created)
32
38
 
33
- - **Edge Scaffold** (generates edge, type, view and controller)
34
- * add `rails generate apache_age:edge_scaffold HasJob employee_role start_node:person end_node:company`
39
+ ## VERSION 0.6.3 - 2024-xx-xx
40
+
41
+ - **Query Sanitize**:
42
+ * reject attributes not defined in model
43
+ * sanitize strings using: id(find) = ?, 23 & find.first_name = ?, 'John'
44
+
45
+ ## VERSION 0.6.2 - 2024-09-30
46
+
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'
53
+
54
+ ## VERSION 0.6.1 - 2024-09-29
55
+
56
+ **Queries are not yet sanitize (injection filtered)!**
57
+
58
+ - **where nodes** - Edge and Node
59
+ - **where edges** - allow subquery on node attributes?
60
+ - **limit** - limit the number of results returned
61
+
62
+ ## VERSION 0.6.0 - 2024-06-28
35
63
 
36
- ## VERSION 0.6.0 - 2024-06-xx
64
+ **Document showing errors**
37
65
 
38
66
  **breaking changes**: update naming
39
67
  * renamed `Entities::Vertex` module to `Entities::Node`
40
68
  * renamed `UniqueVertex` to `UniqueNode`
41
- * rebamed `AgeTypeGenerator` to `Type::Factory`
69
+ * rebamed `AgeTypeGenerator.create_type_for` to `Type::Factory.type_for`
42
70
  * move `lib/generators/*` intp `lib/apache_age/generators`
43
71
 
44
72
  here is the [commit](https://github.com/marpori/rails_age_demo_app/commit/a6f0708f2bbc165eddbafe63896068a72d803b17) to see the changes te demo app to make it work for release 0.6.0
data/README.md CHANGED
@@ -147,24 +147,260 @@ rails generate apache_age:scaffold_node Person first_name last_name
147
147
  rails generate apache_age:scaffold_node Animals/Pet pet_name birthdate:date
148
148
  ```
149
149
 
150
- ### EDGE Scaffold Generation**
151
-
152
- NOTE: the generator will only allow `:node` (default type) for start_node and end_node, however, it is strongly recommended to specify the start_node and end_node types manually. _Hopefully, I can find a way to get the generators to recognize and allow the usage of custom node types. Thus eventually, I hope: `rails generate apache_age:node HasPet start_node:person end_node:pet caretaker_role` will work._
153
-
150
+ now you can test your nodes at:
154
151
  ```bash
155
- rails generate apache_age:edge HasJob employee_role begin_date:date
152
+ http://localhost/people
153
+ # and
154
+ http://localhost/animals_pets
156
155
  ```
157
156
 
158
- _edge scaffold is coming soon._
157
+ Note: Turbo seems to interfere with the default rails template's ability to show errors, this can easily be fixed by disabling turbo for forms that where turbo isn't needed by adding `data: { turbo: false }` to the form, ie:
158
+ ```ruby
159
+ <%= form_with(model: animals_pet, data: { turbo: false }) do |form| %>
160
+ ...
161
+ <% end %>
162
+ ```
163
+
164
+ ### EDGE Scaffold Generation**
159
165
 
160
166
  ```bash
161
167
  # without a namespace
162
168
  rails generate apache_age:scaffold_edge HasPet caretaker_role
169
+ rails generate apache_age:edge HasJob employee_role begin_date:date
163
170
 
164
171
  # with a namespace
165
- rails generate apache_age:scaffold_edge People/HasSpouse spousal_role
172
+ rails generate apache_age:scaffold_edge People/HasSpouce spousal_role
166
173
  ```
167
174
 
175
+ now you can test your edges at:
176
+ ```bash
177
+ http://localhost/has_pets
178
+ http://localhost/has_jobs
179
+ # and
180
+ http://localhost/people/has_spouses
181
+ ```
182
+
183
+ you can improve the view to only show the items you expect to be associated with the start- and end-node by changing the selects in the form from the generic form (finds all nodes):
184
+ ```ruby
185
+ <div>
186
+ <%= form.label :end_node, style: "display: block" %>
187
+ <%= form.collection_select(:end_id, ApacheAge::Node.all, :id, :display, prompt: 'Select an End-Node') %>
188
+ </div>
189
+ ```
190
+ to selecting a specific node expected (along with the desired 'name' in the list)
191
+ ```ruby
192
+ <div>
193
+ <%= form.label :end_node, style: "display: block" %>
194
+ <%= form.collection_select(:end_id, Company.all, :id, :company_name, prompt: 'Select a Company') %>
195
+ </div>
196
+ ```
197
+ so full form change for has_job could look like:
198
+ ```ruby
199
+ # app/views/has_jobs/_form.html.erb
200
+ <%= form_with(model: has_job, url: form_url) do |form| %>
201
+ <% if has_job.errors.any? %>
202
+ <div style="color: red">
203
+ <h2><%= pluralize(has_job.errors.count, "error") %> prohibited this has_job from being saved:</h2>
204
+
205
+ <ul>
206
+ <% has_job.errors.each do |error| %>
207
+ <li><%= error.full_message %></li>
208
+ <% end %>
209
+ </ul>
210
+ </div>
211
+ <% end %>
212
+
213
+ <div>
214
+ <%= form.label :employee_role, style: "display: block" %>
215
+ <%= form.text_field :employee_role %>
216
+ </div>
217
+
218
+ <div>
219
+ <%= form.label :start_node, style: "display: block" %>
220
+ <%= form.collection_select(:start_id, Person.all, :id, :first_name, prompt: 'Select a person') %>
221
+ </div>
222
+
223
+ <div>
224
+ <%= form.label :end_node, style: "display: block" %>
225
+ <%= form.collection_select(:end_id, Company.all, :id, :company_name, prompt: 'Select a Company') %>
226
+ </div>
227
+
228
+ <div>
229
+ <%= form.submit %>
230
+ </div>
231
+ <% end %>
232
+ ```
233
+
234
+ To make your code more robust (enforce that the appropriate node type is associate with the start- and end-nodes) you can adjust the edge definition by adding the node type to the `start_node` and `end_node` attributes.
235
+ from
236
+ ```ruby
237
+ attribute :start_node
238
+ attribute :end_node
239
+ ```
240
+ to:
241
+ ```ruby
242
+ attribute :start_node, :person
243
+ attribute :end_node, :company
244
+ ```
245
+ For example you can make the edge/has_pet.rb more robust by making the model look like:
246
+ ```ruby
247
+ # app/edges/has_job.rb
248
+ class HasJob
249
+ include ApacheAge::Entities::Edge
250
+
251
+ attribute :employee_role, :string
252
+ attribute :start_node, :person
253
+ attribute :end_node, :company
254
+
255
+ validates :employee_role, presence: true
256
+ validate :validate_unique
257
+
258
+ private
259
+
260
+ def validate_unique
261
+ ApacheAge::Validators::UniqueEdge
262
+ .new(attributes: %i[employee_role start_node end_node])
263
+ .validate(self)
264
+ end
265
+ end
266
+ ```
267
+
268
+ The generator will only allow `:node` (default type) since at the time of running the generator (at least within tests, the custom types are not known), eventually, I hope to find a way to fix that and allow:
269
+ `rails generate apache_age:node HasPet start_node:person end_node:pet caretaker_role`
270
+ but that doesn't work yet!
271
+
272
+ ### AGE Rails Quick Example
273
+
274
+ ```bash
275
+ rails new stone_age --database=postresql
276
+ cd stone_age
277
+
278
+ bundle add rails_age
279
+ bundle install
280
+ bin/rails apache_age:install
281
+ bin/rails apache_age:override_db_migrate
282
+ rails db:create
283
+ rails db:migrate
284
+ rails generate apache_age:scaffold_node Person first_name, last_name, gender
285
+ rails generate apache_age:scaffold_node Pet name gender species
286
+ rails generate apache_age:scaffold_edge HasChild role:string
287
+ rails generate apache_age:scaffold_edge HasSibling role:string
288
+ rails generate apache_age:scaffold_edge HasSpouse role:string
289
+
290
+ # seed file: [db/seed.rb](SEED.md)
291
+ rails db:seed
292
+
293
+ # Console Usage (seed doesn't provide any pets)
294
+ dino = Pet.create(name: 'Dino', gender: 'male', species: 'dinosaur')
295
+ dino.to_h
296
+
297
+ # find a person
298
+ fred = Person.find_by(first_name: 'Fred', last_name: 'Flintstone')
299
+ fred.to_h
300
+
301
+ pebbles = Person.find_by(first_name: 'Pebbles')
302
+ pebbles.to_h
303
+
304
+ # find an edge
305
+ father_relationship = HasChild.find_by(start_node: fred, end_node: pebbles)
306
+ father_relationship.to_h
307
+ > {:id=>1407374883553310,
308
+ :end_id=>844424930131996,
309
+ :start_id=>844424930131986,
310
+ :role=>"father",
311
+ :end_node=>{:id=>844424930131996, :last_name=>"Flintstone", :first_name=>"Pebbles", :gender=>"female"},
312
+ :start_node=>{:id=>844424930131986, :last_name=>"Flintstone", :first_name=>"Fred", :gender=>"male"}}
313
+
314
+ # where - find multiple nodes
315
+ family = Person.where(last_name: 'Flintstone').order(:first_name).limit(4).all.puts family.map(&:to_h)
316
+
317
+ family
318
+ > [{:id=>844424930131974, :last_name=>"Flintstone", :first_name=>"Ed", :gender=>"male"},
319
+ > {:id=>844424930131976, :last_name=>"Flintstone", :first_name=>"Edna", :gender=>"female"},
320
+ ? {:id=>844424930131986, :last_name=>"Flintstone", :first_name=>"Fred", :gender=>"male"},
321
+ > {:id=>844424930131975, :last_name=>"Flintstone", :first_name=>"Giggles", :gender=>"male"}]
322
+
323
+ # all - unsorted
324
+ all_family = Person.where(last_name: 'Flintstone').all
325
+ puts all_family.map(&:to_h)
326
+ > {:id=>844424930131969, :last_name=>"Flintstone", :first_name=>"Zeke", :gender=>"female"}
327
+ > {:id=>844424930131970, :last_name=>"Flintstone", :first_name=>"Jed", :gender=>"male"}
328
+ > {:id=>844424930131971, :last_name=>"Flintstone", :first_name=>"Rockbottom", :gender=>"male"}
329
+ > {:id=>844424930131974, :last_name=>"Flintstone", :first_name=>"Ed", :gender=>"male"}
330
+ > {:id=>844424930131975, :last_name=>"Flintstone", :first_name=>"Giggles", :gender=>"male"}
331
+ > {:id=>844424930131976, :last_name=>"Flintstone", :first_name=>"Edna", :gender=>"female"}
332
+ > {:id=>844424930131986, :last_name=>"Flintstone", :first_name=>"Fred", :gender=>"male"}
333
+ > {:id=>844424930131987, :last_name=>"Flintstone", :first_name=>"Wilma", :gender=>"female"}
334
+ > {:id=>844424930131995, :last_name=>"Flintstone", :first_name=>"Stoney", :gender=>"male"}
335
+ > {:id=>844424930131996, :last_name=>"Flintstone", :first_name=>"Pebbles", :gender=>"female"}
336
+
337
+ # where - multiple edges (relations) - for now only edge attributes and start/end nodes can be queried
338
+ parental_relations = HasChild.where(end_node: pebbles)
339
+ puts parental_relations.map(&:to_h)
340
+ > {:id=>1407374883553310, :end_id=>844424930131996, :start_id=>844424930131986, :role=>"father", :end_node=>{:id=>844424930131996, :last_name=>"Flintstone", :first_name=>"Pebbles", :gender=>"female"}, :start_node=>{:id=>844424930131986, :last_name=>"Flintstone", :first_name=>"Fred", :gender=>"male"}}
341
+ > {:id=>1407374883553309, :end_id=>844424930131996, :start_id=>844424930131987, :role=>"mother", :end_node=>{:id=>844424930131996, :last_name=>"Flintstone", :first_name=>"Pebbles", :gender=>"female"}, :start_node=>{:id=>844424930131987, :last_name=>"Flintstone", :first_name=>"Wilma", :gender=>"female"}}
342
+
343
+
344
+ raw_pg_results = Person.where(last_name: 'Flintstone').order(:first_name).limit(4).execute
345
+ => #<PG::Result:0x000000012255f348 status=PGRES_TUPLES_OK ntuples=4 nfields=1 cmd_tuples=4>
346
+ raw_pg_results.values
347
+ > [["{\"id\": 844424930131974, \"label\": \"Person\", \"properties\": {\"gender\": \"male\", \"last_name\": \"Flintstone\", \"first_name\": \"Ed\"}}::vertex"],
348
+ > ["{\"id\": 844424930131976, \"label\": \"Person\", \"properties\": {\"gender\": \"female\", \"last_name\": \"Flintstone\", \"first_name\": \"Edna\"}}::vertex"],
349
+ > ["{\"id\": 844424930131986, \"label\": \"Person\", \"properties\": {\"gender\": \"male\", \"last_name\": \"Flintstone\", \"first_name\": \"Fred\"}}::vertex"],
350
+ > ["{\"id\": 844424930131975, \"label\": \"Person\", \"properties\": {\"gender\": \"male\", \"last_name\": \"Flintstone\", \"first_name\": \"Giggles\"}}::vertex"]]
351
+ ```
352
+
353
+ ### Age Cypher Queries
354
+
355
+ ```ruby
356
+ flintstone_family =
357
+ Person.where(last_name: 'Flintstone')
358
+ .order(:first_name)
359
+ .limit(4).all
360
+ .map(&:to_h)
361
+
362
+ # generates the query
363
+ SELECT *
364
+ FROM cypher('age_schema', $$
365
+ MATCH (find:Person)
366
+ WHERE find.last_name = 'Flintstone'
367
+ RETURN find
368
+ ORDER BY find.first_name
369
+ LIMIT 4
370
+ $$) as (Person agtype);
371
+
372
+ # and returns:
373
+ [{:id=>844424930131974, :last_name=>"Flintstone", :first_name=>"Ed", :gender=>"male"},
374
+ {:id=>844424930131976, :last_name=>"Flintstone", :first_name=>"Edna", :gender=>"female"},
375
+ {:id=>844424930131986, :last_name=>"Flintstone", :first_name=>"Fred", :gender=>"male"},
376
+ {:id=>844424930131975, :last_name=>"Flintstone", :first_name=>"Giggles", :gender=>"male"}]
377
+ ```
378
+
379
+ ```ruby
380
+ query =
381
+ Person.
382
+ cypher('age_schema')
383
+ .match("(a:Person), (b:Person)")
384
+ .where("a.name = 'Node A'", "b.name = 'Node B'")
385
+ .return("a.name", "b.name")
386
+ .as("name_a agtype, name_b agtype")
387
+ .execute
388
+ ```
389
+
390
+ or more generally (soon - not yet tested nor santized):
391
+
392
+ ```ruby
393
+ tihen =
394
+ ApacheAge::Cypher
395
+ .new('age_schema')
396
+ .create("(person:Person {name: 'Tihen'})")
397
+ .return('person')
398
+ .as('Person agtype')
399
+ .execute
400
+ ```
401
+
402
+ see [AGE Cypher Queries](AGE_CYPHER_QUERIES.md)
403
+
168
404
  ### AGE Usage within Rails Console
169
405
 
170
406
  see [AGE Usage within Rails Console](AGE_CONSOLE_USAGE.md)
@@ -0,0 +1,95 @@
1
+ module ApacheAge
2
+ class Cypher
3
+ class << self
4
+ attr_accessor :model_class
5
+ end
6
+
7
+ def initialize(graph_name = 'age_schema')
8
+ @graph_name = graph_name
9
+ @query = ""
10
+ @as_type = "result agtype"
11
+ end
12
+
13
+ def match(pattern)
14
+ @query += "MATCH #{pattern} "
15
+ self
16
+ end
17
+
18
+ # WITH n.name as name, n.age as age
19
+ # WITH otherPerson, count(*) AS foaf WHERE foaf > 1
20
+ # with has a lot of cases - see docs
21
+ def with(*conditions)
22
+ @query += "WITH #{variables.join(', ')} "
23
+ self
24
+ end
25
+
26
+ def where(*conditions)
27
+ condition_str = conditions.join(' AND ')
28
+ # @query += "WHERE #{condition_str} "
29
+ # If there's already a WHERE clause in the query, append to it (they need to be adjacent!)
30
+ @query += (@query.include?("WHERE") ? " AND #{condition_str} " : "WHERE #{condition_str} ")
31
+ self
32
+ end
33
+
34
+ # ORDER BY n.age DESC, n.name ASC
35
+ def order_by(*conditions)
36
+ @query += "ORDER BY #{variables.join(', ')} "
37
+ self
38
+ end
39
+
40
+ # can use full names n.name or aliases (with) name
41
+ def return(*variables)
42
+ @query += "RETURN #{variables.join(', ')} "
43
+ self
44
+ end
45
+
46
+ def create(node)
47
+ @query += "CREATE #{node} "
48
+ self
49
+ end
50
+
51
+ def set(properties)
52
+ @query += "SET #{properties} "
53
+ self
54
+ end
55
+
56
+ def remove(property)
57
+ @query += "REMOVE #{property} "
58
+ self
59
+ end
60
+
61
+ def delete(entity)
62
+ @query += "DELETE #{entity} "
63
+ self
64
+ end
65
+
66
+ def merge(pattern)
67
+ @query += "MERGE #{pattern} "
68
+ self
69
+ end
70
+
71
+ def skip(count)
72
+ @query += "SKIP #{count} "
73
+ self
74
+ end
75
+
76
+ def limit(count)
77
+ @query += "LIMIT #{count} "
78
+ self
79
+ end
80
+
81
+ def as(type)
82
+ @as_type = type
83
+ self
84
+ end
85
+
86
+ def to_cypher
87
+ "SELECT * FROM cypher('#{@graph_name}', $$ #{@query.strip} $$) AS (#{@as_type});"
88
+ end
89
+
90
+ def execute
91
+ cypher_sql = to_cypher
92
+ ActiveRecord::Base.connection.execute(cypher_sql)
93
+ end
94
+ end
95
+ end
@@ -8,58 +8,44 @@ module ApacheAge
8
8
  instance
9
9
  end
10
10
 
11
- def find_by(attributes)
12
- return nil if attributes.reject{ |k,v| v.blank? }.empty?
13
-
14
- edge_keys = [:start_id, :start_node, :end_id, :end_node]
15
- return find_edge(attributes) if edge_keys.any? { |key| attributes.include?(key) }
16
-
17
- where_clause = attributes.map { |k, v| "find.#{k} = '#{v}'" }.join(' AND ')
18
- cypher_sql = find_sql(where_clause)
19
-
20
- execute_find(cypher_sql)
11
+ def where(attributes)
12
+ query_builder = QueryBuilder.new(self)
13
+ query_builder.where(attributes)
21
14
  end
22
15
 
23
- def find(id)
24
- where_clause = "id(find) = #{id}"
25
- cypher_sql = find_sql(where_clause)
26
- execute_find(cypher_sql)
27
- end
28
-
29
- def all
30
- age_results = ActiveRecord::Base.connection.execute(all_sql)
31
- return [] if age_results.values.count.zero?
16
+ def all = QueryBuilder.new(self).all
17
+ def first = QueryBuilder.new(self).limit(1).first
18
+ def find(id) = where(id: id).first
32
19
 
33
- age_results.values.map do |result|
34
- json_string = result.first.split('::').first
35
- hash = JSON.parse(json_string)
36
- attribs = hash.except('label', 'properties').merge(hash['properties']).symbolize_keys
20
+ def find_by(attributes)
21
+ return nil if attributes.reject { |k, v| v.blank? }.empty?
37
22
 
38
- new(**attribs)
39
- end
23
+ where(attributes).limit(1).first
40
24
  end
41
25
 
42
26
  # Private stuff
43
27
 
44
- def find_edge(attributes)
28
+ # used? or dead code?
29
+ def where_edge(attributes)
45
30
  where_attribs =
46
31
  attributes
47
- .compact
48
- .except(:end_id, :start_id, :end_node, :start_node)
49
- .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 ')
50
36
  where_attribs = where_attribs.empty? ? nil : where_attribs
51
37
 
52
38
  end_id = attributes[:end_id] || attributes[:end_node]&.id
53
39
  start_id = attributes[:start_id] || attributes[:start_node]&.id
54
- where_end_id = end_id ? "id(end_node) = #{end_id}" : nil
55
- 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
56
42
 
57
43
  where_clause = [where_attribs, where_start_id, where_end_id].compact.join(' AND ')
58
44
  return nil if where_clause.empty?
59
45
 
60
- cypher_sql = find_edge_sql(where_clause)
46
+ cypher_sql = edge_sql(where_clause)
61
47
 
62
- execute_find(cypher_sql)
48
+ execute_where(cypher_sql)
63
49
  end
64
50
 
65
51
  def age_graph = 'age_schema'
@@ -70,17 +56,21 @@ module ApacheAge
70
56
  age_type == 'vertex' ? "(find:#{age_label})" : "(start_node)-[find:#{age_label}]->(end_node)"
71
57
  end
72
58
 
73
- def execute_find(cypher_sql)
74
- age_result = ActiveRecord::Base.connection.execute(cypher_sql)
75
- return nil if age_result.values.count.zero?
59
+ def execute_sql(cypher_sql) = ActiveRecord::Base.connection.execute(cypher_sql)
60
+
61
+ def execute_find(cypher_sql) = execute_where(cypher_sql).first
76
62
 
77
- age_type = age_result.values.first.first.split('::').last
78
- json_data = age_result.values.first.first.split('::').first
63
+ def execute_where(cypher_sql)
64
+ age_results = ActiveRecord::Base.connection.execute(cypher_sql)
65
+ return [] if age_results.values.count.zero?
79
66
 
80
- hash = JSON.parse(json_data)
81
- attribs = hash.except('label', 'properties').merge(hash['properties']).symbolize_keys
67
+ age_results.values.map do |value|
68
+ json_data = value.first.split('::').first
69
+ hash = JSON.parse(json_data)
70
+ attribs = hash.except('label', 'properties').merge(hash['properties']).symbolize_keys
82
71
 
83
- new(**attribs)
72
+ new(**attribs)
73
+ end
84
74
  end
85
75
 
86
76
  def all_sql
@@ -93,7 +83,7 @@ module ApacheAge
93
83
  SQL
94
84
  end
95
85
 
96
- def find_sql(where_clause)
86
+ def node_sql(where_clause)
97
87
  <<-SQL
98
88
  SELECT *
99
89
  FROM cypher('#{age_graph}', $$
@@ -104,7 +94,7 @@ module ApacheAge
104
94
  SQL
105
95
  end
106
96
 
107
- def find_edge_sql(where_clause)
97
+ def edge_sql(where_clause)
108
98
  <<-SQL
109
99
  SELECT *
110
100
  FROM cypher('#{age_graph}', $$
@@ -114,6 +104,56 @@ module ApacheAge
114
104
  $$) as (#{age_label} agtype);
115
105
  SQL
116
106
  end
107
+
108
+ private
109
+
110
+ def where_node_clause(attributes)
111
+ build_core_where_clause(attributes)
112
+ end
113
+
114
+ def where_edge_clause(attributes)
115
+ core_attributes = attributes.except(:end_id, :start_id, :end_node, :start_node)
116
+ core_clauses = core_attributes.empty? ? nil : build_core_where_clause(core_attributes)
117
+
118
+ end_id =
119
+ if attributes[:end_id]
120
+ attributes[:end_id]
121
+ elsif attributes[:end_node].is_a?(Node)
122
+ attributes[:end_node]&.id
123
+ end
124
+ where_end_id = end_id ? ActiveRecord::Base.sanitize_sql(["id(end_node) = ?", end_id]) : nil
125
+
126
+ start_id =
127
+ if attributes[:start_id]
128
+ attributes[:start_id]
129
+ elsif attributes[:start_node].is_a?(Node)
130
+ attributes[:start_node]&.id
131
+ end
132
+ where_start_id = start_id ? ActiveRecord::Base.sanitize_sql(["id(start_node) = ?", start_id]) : nil
133
+
134
+ where_end_attrs =
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
138
+ where_start_attrs =
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
142
+
143
+ [core_clauses, where_start_id, where_end_id, where_start_attrs, where_end_attrs]
144
+ .flatten.compact.join(' AND ')
145
+ end
146
+
147
+
148
+ def build_core_where_clause(attributes)
149
+ attributes
150
+ .compact
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
155
+ .join(' AND ')
156
+ end
117
157
  end
118
158
  end
119
159
  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
@@ -7,6 +7,8 @@ module ApacheAge
7
7
  include ActiveModel::Model
8
8
  include ActiveModel::Dirty
9
9
  include ActiveModel::Attributes
10
+ include ActiveModel::Validations
11
+ include ActiveModel::Validations::Callbacks
10
12
 
11
13
  attribute :id, :integer
12
14
  # attribute :label, :string
@@ -68,12 +70,25 @@ module ApacheAge
68
70
  def create_sql
69
71
  self.start_node = start_node.save unless start_node.persisted?
70
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(', ')
71
86
  <<-SQL
72
87
  SELECT *
73
88
  FROM cypher('#{age_graph}', $$
74
- MATCH (from_node:#{start_node.age_label}), (to_node:#{end_node.age_label})
75
- WHERE id(from_node) = #{start_node.id} and id(to_node) = #{end_node.id}
76
- 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)
77
92
  RETURN edge
78
93
  $$) as (edge agtype);
79
94
  SQL
@@ -82,14 +97,24 @@ module ApacheAge
82
97
  # So far just properties of string type with '' around them
83
98
  def update_sql
84
99
  alias_name = age_alias || age_label.downcase
85
- set_caluse =
86
- 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
+
87
112
  <<-SQL
88
113
  SELECT *
89
114
  FROM cypher('#{age_graph}', $$
90
115
  MATCH ()-[#{alias_name}:#{age_label}]->()
91
- WHERE id(#{alias_name}) = #{id}
92
- SET #{set_caluse}
116
+ WHERE id(#{alias_name}) = #{sanitized_id}
117
+ SET #{set_clause}
93
118
  RETURN #{alias_name}
94
119
  $$) as (#{age_label} agtype);
95
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
@@ -7,6 +7,8 @@ module ApacheAge
7
7
  include ActiveModel::Model
8
8
  include ActiveModel::Dirty
9
9
  include ActiveModel::Attributes
10
+ include ActiveModel::Validations
11
+ include ActiveModel::Validations::Callbacks
10
12
 
11
13
  attribute :id, :integer
12
14
 
@@ -23,11 +25,21 @@ module ApacheAge
23
25
  # RETURN company
24
26
  # $$) as (Company agtype);
25
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
+
26
32
  alias_name = age_alias || age_label.downcase
27
- <<-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
28
40
  SELECT *
29
41
  FROM cypher('#{age_graph}', $$
30
- CREATE (#{alias_name}#{self})
42
+ CREATE (#{alias_name}:#{age_label} {#{sanitized_properties}})
31
43
  RETURN #{alias_name}
32
44
  $$) as (#{age_label} agtype);
33
45
  SQL
@@ -35,19 +47,29 @@ module ApacheAge
35
47
 
36
48
  # So far just properties of string type with '' around them
37
49
  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(', ')
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
+
41
62
  <<-SQL
42
63
  SELECT *
43
64
  FROM cypher('#{age_graph}', $$
44
65
  MATCH (#{alias_name}:#{age_label})
45
- WHERE id(#{alias_name}) = #{id}
46
- SET #{set_caluse}
66
+ WHERE id(#{alias_name}) = #{sanitized_id}
67
+ SET #{sanitized_set_clause}
47
68
  RETURN #{alias_name}
48
69
  $$) as (#{age_label} agtype);
49
70
  SQL
50
71
  end
72
+
51
73
  end
52
74
  end
53
75
  end
@@ -0,0 +1,27 @@
1
+ module ApacheAge
2
+ module Entities
3
+ module Path
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
+ # attribute :label, :string
13
+ attribute :end_id, :integer
14
+ attribute :start_id, :integer
15
+ # override with a specific node type in the defining class
16
+ attribute :end_node
17
+ attribute :start_node
18
+
19
+ validates :end_node, :start_node, presence: true
20
+ validate :validate_nodes
21
+
22
+ extend ApacheAge::Entities::ClassMethods
23
+ include ApacheAge::Entities::CommonMethods
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,180 @@
1
+ # query =
2
+ # Person.
3
+ # cypher('age_schema')
4
+ # .match("(a:Person), (b:Person)")
5
+ # .where("a.name = 'Node A'", "b.name = 'Node B'")
6
+ # .return("a.name", "b.name")
7
+ # .as("name_a agtype, name_b agtype")
8
+ # .execute
9
+ # def cypher(graph_name = 'age_schema')
10
+ # ApacheAge::Cypher.new(graph_name)
11
+ # self
12
+ # end
13
+
14
+ module ApacheAge
15
+ module Entities
16
+ class QueryBuilder
17
+ attr_accessor :where_clauses, :order_clause, :limit_clause, :model_class, :match_clause,
18
+ :graph_name, :return_clause, :return_names, :return_variables
19
+
20
+ def initialize(model_class, graph_name: nil)
21
+ @model_class = model_class
22
+ @where_clauses = []
23
+ @return_names = ['find']
24
+ @return_clause = 'find'
25
+ @return_variables = []
26
+ @order_clause = nil
27
+ @limit_clause = nil
28
+ @match_clause = model_class.match_clause
29
+ @graph_name = graph_name || model_class.age_graph
30
+ end
31
+
32
+ # def cypher(graph_name = 'age_schema')
33
+ # return self if graph_name.blank?
34
+
35
+ # @graph_name = graph_name
36
+ # self
37
+ # end
38
+
39
+ def match(match_string)
40
+ @match_clause = match_string
41
+ self
42
+ end
43
+
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])
48
+ def where(attributes)
49
+ return self if attributes.blank?
50
+
51
+ @where_clauses <<
52
+ if attributes.is_a?(String)
53
+ if attributes.include?('id(') || attributes.include?('find.')
54
+ attributes
55
+ else
56
+ "find.#{attributes}"
57
+ end
58
+ else
59
+ edge_keys = [:start_id, :start_node, :end_id, :end_node]
60
+ if edge_keys.any? { |key| attributes.include?(key) }
61
+ model_class.send(:where_edge_clause, attributes)
62
+ else
63
+ model_class.send(:where_node_clause, attributes)
64
+ end
65
+ end
66
+
67
+ self
68
+ end
69
+
70
+ # New return method
71
+ def return(*variables)
72
+ return self if variables.blank?
73
+
74
+ @return_variables = variables
75
+ # @return_names = variables.empty? ? ['find'] : variables
76
+ # @return_clause = variables.empty? ? 'find' : "find.#{variables.join(', find.')}"
77
+ self
78
+ end
79
+
80
+ def order(ordering)
81
+ @order_clause = nil
82
+ return self if ordering.blank?
83
+
84
+ order_by_values = Array.wrap(ordering).map { |order| parse_ordering(order) }.join(', ')
85
+ @order_clause = "ORDER BY #{order_by_values}"
86
+ self
87
+ end
88
+
89
+ def limit(limit_value)
90
+ @limit_clause = "LIMIT #{limit_value}"
91
+ self
92
+ end
93
+
94
+ def all
95
+ cypher_sql = build_query
96
+ results = model_class.send(:execute_where, cypher_sql)
97
+ return results if return_variables.empty?
98
+
99
+ results.map(&:to_h).map { _1.slice(*return_variables) }
100
+ end
101
+
102
+ def execute
103
+ cypher_sql = build_query
104
+ model_class.send(:execute_sql, cypher_sql)
105
+ end
106
+
107
+ def first
108
+ cypher_sql = build_query(limit_clause || "LIMIT 1")
109
+ model_class.send(:execute_find, cypher_sql)
110
+ end
111
+
112
+ def to_sql
113
+ build_query.strip
114
+ end
115
+
116
+ private
117
+
118
+ # TODO: ensure ordering keys are present in the model
119
+ def parse_ordering(ordering)
120
+ if ordering.is_a?(Hash)
121
+ ordering =
122
+ ordering
123
+ .map { |k, v| "find.#{k} #{ActiveRecord::Base.sanitize_sql_like(v.to_s)}" }
124
+ .join(', ')
125
+ elsif ordering.is_a?(Symbol)
126
+ ordering = "find.#{ordering}"
127
+ elsif ordering.is_a?(String)
128
+ ordering
129
+ elsif ordering.is_a?(Array)
130
+ ordering = ordering.map do |order|
131
+ if order.is_a?(Hash)
132
+ order
133
+ .map { |k, v| "find.#{k} #{ActiveRecord::Base.sanitize_sql_like(v.to_s)}" }
134
+ .join(', ')
135
+ elsif order.is_a?(Symbol)
136
+ "find.#{order}"
137
+ elsif order.is_a?(String)
138
+ order
139
+ else
140
+ raise ArgumentError, 'Array elements must be a string, symbol, or hash'
141
+ end
142
+ end.join(', ')
143
+ else
144
+ raise ArgumentError, 'Ordering must be a string, symbol, hash, or array'
145
+ end
146
+ end
147
+
148
+ def build_query(_extra_clause = nil)
149
+ where_sql = where_clauses.any? ? "WHERE #{where_clauses.join(' AND ')}" : ''
150
+ order_by = order_clause.present? ? order_clause : ''
151
+ <<-SQL.squish
152
+ SELECT *
153
+ FROM cypher('#{graph_name}', $$
154
+ MATCH #{match_clause}
155
+ #{where_sql}
156
+ RETURN #{return_clause}
157
+ #{order_clause}
158
+ #{limit_clause}
159
+ $$) AS (#{return_names.join(' agtype, ')} agtype);
160
+ SQL
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
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,4 @@
1
+ module ApacheAge
2
+ class Path
3
+ end
4
+ end
@@ -1,3 +1,3 @@
1
1
  module RailsAge
2
- VERSION = '0.6.0'
2
+ VERSION = '0.6.2'
3
3
  end
data/lib/rails_age.rb CHANGED
@@ -6,13 +6,17 @@ module RailsAge
6
6
  end
7
7
 
8
8
  module ApacheAge
9
+ require 'apache_age/cypher.rb'
10
+ require 'apache_age/entities/query_builder'
9
11
  require 'apache_age/entities/class_methods'
10
12
  require 'apache_age/entities/common_methods'
11
13
  require 'apache_age/entities/entity'
12
14
  require 'apache_age/entities/node'
13
15
  require 'apache_age/entities/edge'
16
+ # require 'apache_age/entities/path'
14
17
  require 'apache_age/node'
15
18
  require 'apache_age/edge'
19
+ # require 'apache_age/path'
16
20
  require 'apache_age/validators/expected_node_type'
17
21
  require 'apache_age/validators/unique_node'
18
22
  require 'apache_age/validators/unique_edge'
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.0
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-06-27 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
@@ -30,6 +30,20 @@ dependencies:
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
32
  version: '9.0'
33
+ - !ruby/object:Gem::Dependency
34
+ name: ostruct
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
33
47
  - !ruby/object:Gem::Dependency
34
48
  name: rspec-rails
35
49
  requirement: !ruby/object:Gem::Requirement
@@ -96,14 +110,17 @@ files:
96
110
  - config/routes.rb
97
111
  - db/migrate/20240521062349_add_apache_age.rb
98
112
  - db/schema.rb
113
+ - lib/apache_age/cypher.rb
99
114
  - lib/apache_age/edge.rb
100
115
  - lib/apache_age/entities/class_methods.rb
101
116
  - lib/apache_age/entities/common_methods.rb
102
117
  - lib/apache_age/entities/edge.rb
103
118
  - lib/apache_age/entities/entity.rb
104
119
  - lib/apache_age/entities/node.rb
105
- - lib/apache_age/entities/vertex.rb
120
+ - lib/apache_age/entities/path.rb
121
+ - lib/apache_age/entities/query_builder.rb
106
122
  - lib/apache_age/node.rb
123
+ - lib/apache_age/path.rb
107
124
  - lib/apache_age/types/factory.rb
108
125
  - lib/apache_age/validators/expected_node_type.rb
109
126
  - lib/apache_age/validators/node_type_validator.rb
@@ -162,14 +179,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
162
179
  requirements:
163
180
  - - ">="
164
181
  - !ruby/object:Gem::Version
165
- version: '3.0'
182
+ version: '3.2'
166
183
  required_rubygems_version: !ruby/object:Gem::Requirement
167
184
  requirements:
168
185
  - - ">="
169
186
  - !ruby/object:Gem::Version
170
187
  version: '0'
171
188
  requirements: []
172
- rubygems_version: 3.5.10
189
+ rubygems_version: 3.5.18
173
190
  signing_key:
174
191
  specification_version: 4
175
192
  summary: Apache AGE plugin for Rails 7.x
@@ -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