rails_age 0.5.3 → 0.6.1

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 (30) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +39 -13
  3. data/README.md +246 -10
  4. data/config/initializers/types.rb +5 -5
  5. data/lib/apache_age/cypher.rb +95 -0
  6. data/lib/apache_age/entities/class_methods.rb +68 -38
  7. data/lib/apache_age/entities/edge.rb +2 -0
  8. data/lib/apache_age/entities/node.rb +3 -1
  9. data/lib/apache_age/entities/path.rb +27 -0
  10. data/lib/apache_age/entities/query_builder.rb +159 -0
  11. data/lib/apache_age/entities/vertex.rb +47 -47
  12. data/lib/apache_age/path.rb +4 -0
  13. data/lib/apache_age/types/{age_type_factory.rb → factory.rb} +3 -3
  14. data/lib/apache_age/validators/{vertex_type_validator copy.rb → node_type_validator.rb} +1 -1
  15. data/lib/apache_age/validators/unique_vertex.rb +27 -27
  16. data/lib/apache_age/validators/vertex_type_validator.rb +15 -0
  17. data/lib/generators/apache_age/edge/USAGE +7 -7
  18. data/lib/generators/apache_age/edge/edge_generator.rb +1 -0
  19. data/lib/generators/apache_age/generator_entity_helpers.rb +1 -9
  20. data/lib/generators/apache_age/node/USAGE +4 -4
  21. data/lib/generators/apache_age/node/node_generator.rb +1 -1
  22. data/lib/generators/apache_age/node/templates/node.rb.tt +2 -2
  23. data/lib/generators/apache_age/scaffold_edge/scaffold_edge_generator.rb +1 -2
  24. data/lib/generators/apache_age/scaffold_node/scaffold_node_generator.rb +1 -2
  25. data/lib/rails_age/version.rb +1 -1
  26. data/lib/rails_age.rb +5 -4
  27. data/lib/tasks/config_types.rake +6 -6
  28. metadata +25 -8
  29. data/lib/apache_age/types/age_type_generator.rb +0 -46
  30. data/lib/generators/apache_age/scaffold_edge/scaffold_node_generator.rb +0 -67
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cd10883497fc245765a9e8313265e4d06c7a602a2f83cad592ecb5a951ab488d
4
- data.tar.gz: ce03c6dc4535e2a54dddc9f2b9df8e4b413c6aeb690ee00138c4120fe1d72ce1
3
+ metadata.gz: 053d21a51d2e91cb61ea1678d2d84c05ac4a1a84a643334c4993df9f6ca9c3c0
4
+ data.tar.gz: 57f945d6055d9dca60414321e3f39dd09abfdb1a8f2a3a5fec0b4c5e3e9ee656
5
5
  SHA512:
6
- metadata.gz: dcb00b35abd3a931c95ffce304c079eee2d1ba1b09ee3aa17f9c9150f4bc3b55276e7670cf3925e4ae484a117bef66bddc0242efc0cbc8295da03538ae628805
7
- data.tar.gz: d6ac9aaa1ba0b04c9a48ed320e53a4de65a74e6cb6574f42c407b0c1124fafc650def68df4c41beaad27607b6f1fa22a7962518108c2fc39cd7c0e95f76d8c09
6
+ metadata.gz: 017c585f995749dad85f341b41a01d2f94b0cc03fac6240d63e4755e8ab0c7148969793fbe9c308f13c0ea94d35eee3fb231516eb775817a53bef003843d41e4
7
+ data.tar.gz: 83760cc109544589665b7297776538f48f4ac0c14273455957e714ff6d9614e1a4e8d8ba862e39b8735a5f1e5a1f9c8ca5e429f1823aee8b98e38bad3dded2de
data/CHANGELOG.md CHANGED
@@ -1,6 +1,20 @@
1
1
  # Change Log
2
2
 
3
- ## VERSION 0.6.2 - 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,11 +22,12 @@
8
22
  * paths support
9
23
  * select attributes support
10
24
 
11
- ## VERSION 0.6.1 - 2024-xx-xx
25
+ ## VERSION 0.7.0 - 2024-xx-xx
12
26
 
13
- - **Age Path**
27
+ - **Age Path** - nodes and edges combined
28
+ * add `rails generate apache_age:path_scaffold HasJob employee_role start_node:person end_node:company`
14
29
 
15
- ## VERSION 0.6.0 - 2024-xx-xx
30
+ ## VERSION 0.6.3 - 2024-xx-xx
16
31
 
17
32
  breaking change?: namespaces (by default) will use their own schema? (add to database.yml & schema.rb ?)
18
33
 
@@ -20,18 +35,29 @@ breaking change?: namespaces (by default) will use their own schema? (add to dat
20
35
 
21
36
  - **multiple AGE Schema**
22
37
 
23
- ## VERSION 0.5.4 - 2024-xx-xx
38
+ ## VERSION 0.6.2 - 2024-xx-xx
24
39
 
25
- - **Fix**
26
- * show validation errors in scaffold views
40
+ - **Query Sanitize**
27
41
 
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)
42
+ ## VERSION 0.6.1 - 2024-09-29
32
43
 
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`
44
+ **Queries are not yet sanitize (injection filtered)!**
45
+
46
+ - **where nodes** - Edge and Node
47
+ - **where edges** - allow subquery on node attributes?
48
+ - **limit** - limit the number of results returned
49
+
50
+ ## VERSION 0.6.0 - 2024-06-28
51
+
52
+ **Document showing errors**
53
+
54
+ **breaking changes**: update naming
55
+ * renamed `Entities::Vertex` module to `Entities::Node`
56
+ * renamed `UniqueVertex` to `UniqueNode`
57
+ * rebamed `AgeTypeGenerator.create_type_for` to `Type::Factory.type_for`
58
+ * move `lib/generators/*` intp `lib/apache_age/generators`
59
+
60
+ 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
35
61
 
36
62
  ## VERSION 0.5.3 - 2024-06-23
37
63
 
data/README.md CHANGED
@@ -32,7 +32,7 @@ rails generate apache_age:scaffold_node Person first_name last_name
32
32
  rails generate apache_age:scaffold_edge HasJob employee_role start_date:date
33
33
  ```
34
34
 
35
- Ideally, edit the HasJob class so that `start_node` would use a type `:person` and the `end_node` uses at type `:company`
35
+ Ideally, edit the HasJob class so that `start_node` would use a type `:person` and the `end_node` uses at type `:company` - this is not yet supported by the generator, but easy to do manually as shown below. (The problem is that I havent been able to figure out how load all the rails types in the testing environment).
36
36
 
37
37
  ie:
38
38
  ```ruby
@@ -41,8 +41,8 @@ class HasJob
41
41
  include ApacheAge::Entities::Edge
42
42
 
43
43
  attribute :employee_role, :string
44
- attribute :start_node, :person # instead of `:node`
45
- attribute :end_node, :company # instead of `:node`
44
+ attribute :start_node, :person
45
+ attribute :end_node, :company
46
46
 
47
47
  validates :employee_role, presence: true
48
48
  validate :validate_unique_edge
@@ -147,23 +147,259 @@ 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
173
+ ```
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>
166
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:
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)
167
403
 
168
404
  ### AGE Usage within Rails Console
169
405
 
@@ -1,20 +1,20 @@
1
1
  # config/initializers/types.rb
2
2
 
3
- require 'apache_age/types/age_type_factory'
3
+ require 'apache_age/types/factory'
4
4
  # USAGE (with edges or nodes) - ie:
5
- # require_dependency 'nodes/company'
5
+ # require_dependency 'company'
6
6
  # ActiveModel::Type.register(
7
- # :company, ApacheAge::Types::AgeTypeFactory.create_type_for(Nodes::Company)
7
+ # :company, ApacheAge::Types::Factory.type_for(Company)
8
8
  # )
9
9
 
10
10
  Rails.application.config.to_prepare do
11
11
  # Register AGE types
12
12
  require_dependency 'apache_age/node'
13
13
  ActiveModel::Type.register(
14
- :node, ApacheAge::Types::AgeTypeFactory.create_type_for(ApacheAge::Node)
14
+ :node, ApacheAge::Types::Factory.type_for(ApacheAge::Node)
15
15
  )
16
16
  require_dependency 'apache_age/edge'
17
17
  ActiveModel::Type.register(
18
- :edge, ApacheAge::Types::AgeTypeFactory.create_type_for(ApacheAge::Edge)
18
+ :edge, ApacheAge::Types::Factory.type_for(ApacheAge::Edge)
19
19
  )
20
20
  end
@@ -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,40 +8,24 @@ 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
+ def where_edge(attributes)
45
29
  where_attribs =
46
30
  attributes
47
31
  .compact
@@ -57,9 +41,9 @@ module ApacheAge
57
41
  where_clause = [where_attribs, where_start_id, where_end_id].compact.join(' AND ')
58
42
  return nil if where_clause.empty?
59
43
 
60
- cypher_sql = find_edge_sql(where_clause)
44
+ cypher_sql = edge_sql(where_clause)
61
45
 
62
- execute_find(cypher_sql)
46
+ execute_where(cypher_sql)
63
47
  end
64
48
 
65
49
  def age_graph = 'age_schema'
@@ -70,17 +54,21 @@ module ApacheAge
70
54
  age_type == 'vertex' ? "(find:#{age_label})" : "(start_node)-[find:#{age_label}]->(end_node)"
71
55
  end
72
56
 
73
- def execute_find(cypher_sql)
74
- age_result = ActiveRecord::Base.connection.execute(cypher_sql)
75
- return nil if age_result.values.count.zero?
57
+ def execute_sql(cypher_sql) = ActiveRecord::Base.connection.execute(cypher_sql)
76
58
 
77
- age_type = age_result.values.first.first.split('::').last
78
- json_data = age_result.values.first.first.split('::').first
59
+ def execute_find(cypher_sql) = execute_where(cypher_sql).first
79
60
 
80
- hash = JSON.parse(json_data)
81
- attribs = hash.except('label', 'properties').merge(hash['properties']).symbolize_keys
61
+ def execute_where(cypher_sql)
62
+ age_results = ActiveRecord::Base.connection.execute(cypher_sql)
63
+ return [] if age_results.values.count.zero?
82
64
 
83
- new(**attribs)
65
+ age_results.values.map do |value|
66
+ json_data = value.first.split('::').first
67
+ hash = JSON.parse(json_data)
68
+ attribs = hash.except('label', 'properties').merge(hash['properties']).symbolize_keys
69
+
70
+ new(**attribs)
71
+ end
84
72
  end
85
73
 
86
74
  def all_sql
@@ -93,7 +81,7 @@ module ApacheAge
93
81
  SQL
94
82
  end
95
83
 
96
- def find_sql(where_clause)
84
+ def node_sql(where_clause)
97
85
  <<-SQL
98
86
  SELECT *
99
87
  FROM cypher('#{age_graph}', $$
@@ -104,7 +92,7 @@ module ApacheAge
104
92
  SQL
105
93
  end
106
94
 
107
- def find_edge_sql(where_clause)
95
+ def edge_sql(where_clause)
108
96
  <<-SQL
109
97
  SELECT *
110
98
  FROM cypher('#{age_graph}', $$
@@ -114,6 +102,48 @@ module ApacheAge
114
102
  $$) as (#{age_label} agtype);
115
103
  SQL
116
104
  end
105
+
106
+ private
107
+
108
+ def where_node_clause(attributes)
109
+ build_core_where_clause(attributes)
110
+ end
111
+
112
+ def where_edge_clause(attributes)
113
+ core_attributes = attributes.except(:end_id, :start_id, :end_node, :start_node)
114
+ core_clauses = core_attributes.empty? ? nil : build_core_where_clause(core_attributes)
115
+
116
+ end_id =
117
+ if attributes[:end_id]
118
+ end_id = attributes[:end_id]
119
+ elsif attributes[:end_node].is_a?(Node)
120
+ end_id = attributes[:end_node]&.id
121
+ end
122
+ where_end_id = end_id ? "id(end_node) = #{end_id}" : nil
123
+
124
+ start_id =
125
+ if attributes[:start_id]
126
+ start_id = attributes[:start_id]
127
+ elsif attributes[:start_node].is_a?(Node)
128
+ start_id = attributes[:start_node]&.id
129
+ end
130
+ where_start_id = start_id ? "id(start_node) = #{start_id}" : nil
131
+
132
+ where_end_attrs =
133
+ attributes[:end_node].map { |k, v| "end_node.#{k} = '#{v}'" } if attributes[:end_node].is_a?(Hash)
134
+ where_start_attrs =
135
+ attributes[:start_node].map { |k, v| "start_node.#{k} = '#{v}'" } if attributes[:start_node].is_a?(Hash)
136
+
137
+ [core_clauses, where_start_id, where_end_id, where_start_attrs, where_end_attrs]
138
+ .flatten.compact.join(' AND ')
139
+ end
140
+
141
+ def build_core_where_clause(attributes)
142
+ attributes
143
+ .compact
144
+ .map { |k, v| k == :id ? "id(find) = #{v}" : "find.#{k} = '#{v}'"}
145
+ .join(' AND ')
146
+ end
117
147
  end
118
148
  end
119
149
  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
@@ -1,12 +1,14 @@
1
1
  module ApacheAge
2
2
  module Entities
3
- module Vertex
3
+ module Node
4
4
  extend ActiveSupport::Concern
5
5
 
6
6
  included do
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