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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +39 -13
- data/README.md +246 -10
- data/config/initializers/types.rb +5 -5
- data/lib/apache_age/cypher.rb +95 -0
- data/lib/apache_age/entities/class_methods.rb +68 -38
- data/lib/apache_age/entities/edge.rb +2 -0
- data/lib/apache_age/entities/node.rb +3 -1
- data/lib/apache_age/entities/path.rb +27 -0
- data/lib/apache_age/entities/query_builder.rb +159 -0
- data/lib/apache_age/entities/vertex.rb +47 -47
- data/lib/apache_age/path.rb +4 -0
- data/lib/apache_age/types/{age_type_factory.rb → factory.rb} +3 -3
- data/lib/apache_age/validators/{vertex_type_validator copy.rb → node_type_validator.rb} +1 -1
- data/lib/apache_age/validators/unique_vertex.rb +27 -27
- data/lib/apache_age/validators/vertex_type_validator.rb +15 -0
- data/lib/generators/apache_age/edge/USAGE +7 -7
- data/lib/generators/apache_age/edge/edge_generator.rb +1 -0
- data/lib/generators/apache_age/generator_entity_helpers.rb +1 -9
- data/lib/generators/apache_age/node/USAGE +4 -4
- data/lib/generators/apache_age/node/node_generator.rb +1 -1
- data/lib/generators/apache_age/node/templates/node.rb.tt +2 -2
- data/lib/generators/apache_age/scaffold_edge/scaffold_edge_generator.rb +1 -2
- data/lib/generators/apache_age/scaffold_node/scaffold_node_generator.rb +1 -2
- data/lib/rails_age/version.rb +1 -1
- data/lib/rails_age.rb +5 -4
- data/lib/tasks/config_types.rake +6 -6
- metadata +25 -8
- data/lib/apache_age/types/age_type_generator.rb +0 -46
- 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:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 053d21a51d2e91cb61ea1678d2d84c05ac4a1a84a643334c4993df9f6ca9c3c0
|
4
|
+
data.tar.gz: 57f945d6055d9dca60414321e3f39dd09abfdb1a8f2a3a5fec0b4c5e3e9ee656
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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.
|
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.
|
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.
|
38
|
+
## VERSION 0.6.2 - 2024-xx-xx
|
24
39
|
|
25
|
-
- **
|
26
|
-
* show validation errors in scaffold views
|
40
|
+
- **Query Sanitize**
|
27
41
|
|
28
|
-
-
|
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
|
-
|
34
|
-
|
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
|
45
|
-
attribute :end_node, :company
|
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
|
-
|
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
|
-
|
152
|
+
http://localhost/people
|
153
|
+
# and
|
154
|
+
http://localhost/animals_pets
|
156
155
|
```
|
157
156
|
|
158
|
-
|
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/
|
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/
|
3
|
+
require 'apache_age/types/factory'
|
4
4
|
# USAGE (with edges or nodes) - ie:
|
5
|
-
# require_dependency '
|
5
|
+
# require_dependency 'company'
|
6
6
|
# ActiveModel::Type.register(
|
7
|
-
# :company, ApacheAge::Types::
|
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::
|
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::
|
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
|
12
|
-
|
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
|
24
|
-
|
25
|
-
|
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
|
-
|
34
|
-
|
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
|
-
|
39
|
-
end
|
23
|
+
where(attributes).limit(1).first
|
40
24
|
end
|
41
25
|
|
42
26
|
# Private stuff
|
43
27
|
|
44
|
-
def
|
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 =
|
44
|
+
cypher_sql = edge_sql(where_clause)
|
61
45
|
|
62
|
-
|
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
|
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
|
-
|
78
|
-
json_data = age_result.values.first.first.split('::').first
|
59
|
+
def execute_find(cypher_sql) = execute_where(cypher_sql).first
|
79
60
|
|
80
|
-
|
81
|
-
|
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
|
-
|
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
|
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
|
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
|
@@ -1,12 +1,14 @@
|
|
1
1
|
module ApacheAge
|
2
2
|
module Entities
|
3
|
-
module
|
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
|
|