rails_age 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +10 -0
- data/MIT-LICENSE +20 -0
- data/README.md +178 -0
- data/Rakefile +8 -0
- data/app/assets/config/rails_age_manifest.js +1 -0
- data/app/assets/stylesheets/rails_age/application.css +15 -0
- data/app/controllers/rails_age/application_controller.rb +4 -0
- data/app/helpers/rails_age/application_helper.rb +4 -0
- data/app/jobs/rails_age/application_job.rb +4 -0
- data/app/mailers/rails_age/application_mailer.rb +6 -0
- data/app/models/rails_age/application_record.rb +5 -0
- data/app/views/layouts/rails_age/application.html.erb +15 -0
- data/config/routes.rb +2 -0
- data/db/migrate/20240521062349_configure_apache_age.rb +35 -0
- data/db/schema.rb +16 -0
- data/lib/apache_age/class_methods.rb +75 -0
- data/lib/apache_age/common_methods.rb +126 -0
- data/lib/apache_age/edge.rb +64 -0
- data/lib/apache_age/entity.rb +67 -0
- data/lib/apache_age/vertex.rb +52 -0
- data/lib/rails_age/engine.rb +5 -0
- data/lib/rails_age/version.rb +3 -0
- data/lib/rails_age.rb +14 -0
- data/lib/tasks/rails_age_tasks.rake +4 -0
- metadata +98 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 45fb3b7f94084761a6a3d6a26c9e4d86f3b790768ba52d5a5735713bb978d37b
|
4
|
+
data.tar.gz: af3866f393670857680d9334280058a0542155f3b76cb2cc64648fa0875e9f92
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 774867c2584780ca6bbae3edbdf82fd592ebb3b3cb65c8f97400ceba5b37db650ce7c0ec9da1b45b30c2cf789e1d2e2ca776b97cbfc8ac431b3af234b4ba9efe
|
7
|
+
data.tar.gz: 05c03b0e12a5c77a53463cdfae58cce43b1f0cddfb811cad4a81e9ff137d49930434f53cbe0f6858e933eca15e9ce7e98b4d7cf57cb24d690a3a0d2b58cfc314
|
data/CHANGELOG.md
ADDED
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright Bill Tihen
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,178 @@
|
|
1
|
+
# RailsAge
|
2
|
+
|
3
|
+
Simplify Apache Age usage within a Rails application.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem "rails_age"
|
11
|
+
```
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
```bash
|
16
|
+
$ bundle
|
17
|
+
```
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
|
21
|
+
```bash
|
22
|
+
$ gem install rails_age
|
23
|
+
```
|
24
|
+
|
25
|
+
## Contributing
|
26
|
+
|
27
|
+
Create an MR and tests and I will review it.
|
28
|
+
|
29
|
+
## License
|
30
|
+
|
31
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
32
|
+
|
33
|
+
## Usage
|
34
|
+
|
35
|
+
I suggest you creat a folder within app called `graphs` and under that create a folder called `nodes` and `edges`. This will help you keep your code organized.
|
36
|
+
|
37
|
+
A full sample app can be found [here](https://github.com/btihen-dev/rails_graphdb_age_app) the summary usage is described below.
|
38
|
+
|
39
|
+
### Nodes
|
40
|
+
|
41
|
+
```ruby
|
42
|
+
# app/graphs/nodes/company.rb
|
43
|
+
module Nodes
|
44
|
+
class Company
|
45
|
+
include ApacheAge::Vertex
|
46
|
+
|
47
|
+
attribute :company_name, :string
|
48
|
+
validates :company_name, presence: true
|
49
|
+
end
|
50
|
+
end
|
51
|
+
```
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
# app/graphs/nodes/person.rb
|
55
|
+
module Nodes
|
56
|
+
class Person
|
57
|
+
include ApacheAge::Vertex
|
58
|
+
|
59
|
+
attribute :first_name, :string, default: nil
|
60
|
+
attribute :last_name, :string, default: nil
|
61
|
+
attribute :given_name, :string, default: nil
|
62
|
+
attribute :nick_name, :string, default: nil
|
63
|
+
attribute :gender, :string, default: nil
|
64
|
+
|
65
|
+
validates :gender, :first_name, :last_name, :given_name, :nick_name,
|
66
|
+
presence: true
|
67
|
+
|
68
|
+
def initialize(**attributes)
|
69
|
+
super
|
70
|
+
# use unless present? since attributes when empty sets to "" by default
|
71
|
+
self.nick_name = first_name unless nick_name.present?
|
72
|
+
self.given_name = last_name unless given_name.present?
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
```
|
77
|
+
|
78
|
+
### Edges
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
# app/graphs/edges/works_at.rb
|
82
|
+
module Edges
|
83
|
+
class WorksAt
|
84
|
+
include ApacheAge::Edge
|
85
|
+
|
86
|
+
attribute :employee_role, :string
|
87
|
+
validates :employee_role, presence: true
|
88
|
+
end
|
89
|
+
end
|
90
|
+
```
|
91
|
+
|
92
|
+
### Rails Console Usage
|
93
|
+
|
94
|
+
```ruby
|
95
|
+
fred = Nodes::Person.create(first_name: 'Fredrick Jay', nick_name: 'Fred', last_name: 'Flintstone', gender: 'male')
|
96
|
+
fred.to_h
|
97
|
+
|
98
|
+
quarry = Nodes::Company.create(company_name: 'Bedrock Quarry')
|
99
|
+
quarry.to_h
|
100
|
+
|
101
|
+
job = Edges::WorksAt.create(start_node: fred, end_node: quarry, employee_role: 'Crane Operator')
|
102
|
+
job.to_h
|
103
|
+
```
|
104
|
+
|
105
|
+
### Controller Usage
|
106
|
+
|
107
|
+
```ruby
|
108
|
+
# app/controllers/people_controller.rb
|
109
|
+
class PeopleController < ApplicationController
|
110
|
+
before_action :set_person, only: %i[show edit update destroy]
|
111
|
+
|
112
|
+
# GET /people or /people.json
|
113
|
+
def index
|
114
|
+
@people = Nodes::Person.all
|
115
|
+
end
|
116
|
+
|
117
|
+
# GET /people/1 or /people/1.json
|
118
|
+
def show; end
|
119
|
+
|
120
|
+
# GET /people/new
|
121
|
+
def new
|
122
|
+
@person = Nodes::Person.new
|
123
|
+
end
|
124
|
+
|
125
|
+
# GET /people/1/edit
|
126
|
+
def edit; end
|
127
|
+
|
128
|
+
# POST /people or /people.json
|
129
|
+
def create
|
130
|
+
@person = Nodes::Person.new(**person_params)
|
131
|
+
respond_to do |format|
|
132
|
+
if @person.save
|
133
|
+
format.html { redirect_to person_url(@person), notice: 'Person was successfully created.' }
|
134
|
+
format.json { render :show, status: :created, location: @person }
|
135
|
+
else
|
136
|
+
format.html { render :new, status: :unprocessable_entity }
|
137
|
+
format.json { render json: @person.errors, status: :unprocessable_entity }
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
# PATCH/PUT /people/1 or /people/1.json
|
143
|
+
def update
|
144
|
+
respond_to do |format|
|
145
|
+
if @person.update(**person_params)
|
146
|
+
format.html { redirect_to person_url(@person), notice: 'Person was successfully updated.' }
|
147
|
+
format.json { render :show, status: :ok, location: @person }
|
148
|
+
else
|
149
|
+
format.html { render :edit, status: :unprocessable_entity }
|
150
|
+
format.json { render json: @person.errors, status: :unprocessable_entity }
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
# DELETE /people/1 or /people/1.json
|
156
|
+
def destroy
|
157
|
+
@person.destroy!
|
158
|
+
|
159
|
+
respond_to do |format|
|
160
|
+
format.html { redirect_to people_url, notice: 'Person was successfully destroyed.' }
|
161
|
+
format.json { head :no_content }
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
private
|
166
|
+
|
167
|
+
# Use callbacks to share common setup or constraints between actions.
|
168
|
+
def set_person
|
169
|
+
@person = Nodes::Person.find(params[:id])
|
170
|
+
end
|
171
|
+
|
172
|
+
# Only allow a list of trusted parameters through.
|
173
|
+
def person_params
|
174
|
+
# params.fetch(:person, {})
|
175
|
+
params.require(:nodes_person).permit(:first_name, :last_name, :nick_name, :given_name, :gender)
|
176
|
+
end
|
177
|
+
end
|
178
|
+
```
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
//= link_directory ../stylesheets/rails_age .css
|
@@ -0,0 +1,15 @@
|
|
1
|
+
/*
|
2
|
+
* This is a manifest file that'll be compiled into application.css, which will include all the files
|
3
|
+
* listed below.
|
4
|
+
*
|
5
|
+
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
|
6
|
+
* or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
|
7
|
+
*
|
8
|
+
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
|
9
|
+
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
|
10
|
+
* files in this directory. Styles in this file should be added after the last require_* statement.
|
11
|
+
* It is generally better to create a new file per style scope.
|
12
|
+
*
|
13
|
+
*= require_tree .
|
14
|
+
*= require_self
|
15
|
+
*/
|
data/config/routes.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
class ConfigureApacheAge < ActiveRecord::Migration[7.1]
|
2
|
+
def up
|
3
|
+
# Allow age extension
|
4
|
+
execute('CREATE EXTENSION IF NOT EXISTS age;')
|
5
|
+
|
6
|
+
# Load the age code
|
7
|
+
execute("LOAD 'age';")
|
8
|
+
|
9
|
+
# Load the ag_catalog into the search path
|
10
|
+
execute('SET search_path = ag_catalog, "$user", public;')
|
11
|
+
|
12
|
+
# Create age_schema graph if it doesn't exist
|
13
|
+
execute("SELECT create_graph('age_schema');")
|
14
|
+
end
|
15
|
+
|
16
|
+
def down
|
17
|
+
execute <<-SQL
|
18
|
+
DO $$
|
19
|
+
BEGIN
|
20
|
+
IF EXISTS (
|
21
|
+
SELECT 1
|
22
|
+
FROM pg_constraint
|
23
|
+
WHERE conname = 'fk_graph_oid'
|
24
|
+
) THEN
|
25
|
+
ALTER TABLE ag_catalog.ag_label
|
26
|
+
DROP CONSTRAINT fk_graph_oid;
|
27
|
+
END IF;
|
28
|
+
END $$;
|
29
|
+
SQL
|
30
|
+
|
31
|
+
execute("SELECT drop_graph('age_schema', true);")
|
32
|
+
execute('DROP SCHEMA IF EXISTS ag_catalog CASCADE;')
|
33
|
+
execute('DROP EXTENSION IF EXISTS age;')
|
34
|
+
end
|
35
|
+
end
|
data/db/schema.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
ActiveRecord::Schema[7.1].define(version: 2024_05_21_062349) do
|
2
|
+
# These are extensions that must be enabled in order to support this database
|
3
|
+
enable_extension "plpgsql"
|
4
|
+
|
5
|
+
# Allow age extension
|
6
|
+
execute('CREATE EXTENSION IF NOT EXISTS age;')
|
7
|
+
|
8
|
+
# Load the age code
|
9
|
+
execute("LOAD 'age';")
|
10
|
+
|
11
|
+
# Load the ag_catalog into the search path
|
12
|
+
execute('SET search_path = ag_catalog, "$user", public;')
|
13
|
+
|
14
|
+
# Create age_schema graph if it doesn't exist
|
15
|
+
execute("SELECT create_graph('age_schema');")
|
16
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
module ApacheAge
|
2
|
+
module ClassMethods
|
3
|
+
# for now we only allow one predertimed graph
|
4
|
+
def create(attributes) = new(**attributes).save
|
5
|
+
|
6
|
+
def find_by(attributes)
|
7
|
+
where_clause = attributes.map { |k, v| "find.#{k} = '#{v}'" }.join(' AND ')
|
8
|
+
cypher_sql = find_sql(where_clause)
|
9
|
+
execute_find(cypher_sql)
|
10
|
+
end
|
11
|
+
|
12
|
+
def find(id)
|
13
|
+
where_clause = "id(find) = #{id}"
|
14
|
+
cypher_sql = find_sql(where_clause)
|
15
|
+
execute_find(cypher_sql)
|
16
|
+
end
|
17
|
+
|
18
|
+
def all
|
19
|
+
age_results = ActiveRecord::Base.connection.execute(all_sql)
|
20
|
+
return [] if age_results.values.count.zero?
|
21
|
+
|
22
|
+
age_results.values.map do |result|
|
23
|
+
json_string = result.first.split('::').first
|
24
|
+
hash = JSON.parse(json_string)
|
25
|
+
attribs = hash.except('label', 'properties').merge(hash['properties']).symbolize_keys
|
26
|
+
|
27
|
+
new(**attribs)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Private stuff
|
32
|
+
|
33
|
+
def age_graph = 'age_schema'
|
34
|
+
def age_label = name.gsub('::', '__')
|
35
|
+
def age_type = name.constantize.new.age_type
|
36
|
+
|
37
|
+
def match_clause
|
38
|
+
age_type == 'vertex' ? "(find:#{age_label})" : "()-[find:#{age_label}]->()"
|
39
|
+
end
|
40
|
+
|
41
|
+
def execute_find(cypher_sql)
|
42
|
+
age_result = ActiveRecord::Base.connection.execute(cypher_sql)
|
43
|
+
return nil if age_result.values.count.zero?
|
44
|
+
|
45
|
+
age_type = age_result.values.first.first.split('::').last
|
46
|
+
json_data = age_result.values.first.first.split('::').first
|
47
|
+
|
48
|
+
hash = JSON.parse(json_data)
|
49
|
+
attribs = hash.except('label', 'properties').merge(hash['properties']).symbolize_keys
|
50
|
+
|
51
|
+
new(**attribs)
|
52
|
+
end
|
53
|
+
|
54
|
+
def all_sql
|
55
|
+
<<-SQL
|
56
|
+
SELECT *
|
57
|
+
FROM cypher('#{age_graph}', $$
|
58
|
+
MATCH #{match_clause}
|
59
|
+
RETURN find
|
60
|
+
$$) as (#{age_label} agtype);
|
61
|
+
SQL
|
62
|
+
end
|
63
|
+
|
64
|
+
def find_sql(where_clause)
|
65
|
+
<<-SQL
|
66
|
+
SELECT *
|
67
|
+
FROM cypher('#{age_graph}', $$
|
68
|
+
MATCH #{match_clause}
|
69
|
+
WHERE #{where_clause}
|
70
|
+
RETURN find
|
71
|
+
$$) as (#{age_label} agtype);
|
72
|
+
SQL
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
module ApacheAge
|
2
|
+
module CommonMethods
|
3
|
+
def initialize(**attributes)
|
4
|
+
super
|
5
|
+
return self unless age_type == 'edge'
|
6
|
+
|
7
|
+
self.end_id ||= end_node.id if end_node
|
8
|
+
self.start_id ||= start_node.id if start_node
|
9
|
+
self.end_node ||= Entity.find(end_id) if end_id
|
10
|
+
self.start_node ||= Entity.find(start_id) if start_id
|
11
|
+
end
|
12
|
+
|
13
|
+
# for now we just can just use one schema
|
14
|
+
def age_graph = 'age_schema'
|
15
|
+
def age_label = self.class.name.gsub('::', '__')
|
16
|
+
def persisted? = id.present?
|
17
|
+
def to_s = ":#{age_label} #{properties_to_s}"
|
18
|
+
|
19
|
+
def to_h
|
20
|
+
base_h = attributes.to_hash
|
21
|
+
if age_type == 'edge'
|
22
|
+
# remove the nodes (in attribute form and re-add in hash form)
|
23
|
+
base_h = base_h.except('start_node', 'end_node')
|
24
|
+
base_h[:end_node] = end_node.to_h if end_node
|
25
|
+
base_h[:start_node] = start_node.to_h if start_node
|
26
|
+
end
|
27
|
+
base_h.symbolize_keys
|
28
|
+
end
|
29
|
+
|
30
|
+
def update_attributes(attribs)
|
31
|
+
attribs.except(id:).each do |key, value|
|
32
|
+
send("#{key}=", value) if respond_to?("#{key}=")
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def update(attribs)
|
37
|
+
update_attributes(attribs)
|
38
|
+
save
|
39
|
+
end
|
40
|
+
|
41
|
+
def save
|
42
|
+
return false unless valid?
|
43
|
+
|
44
|
+
cypher_sql = (persisted? ? update_sql : create_sql)
|
45
|
+
response_hash = execute_sql(cypher_sql)
|
46
|
+
|
47
|
+
self.id = response_hash['id']
|
48
|
+
|
49
|
+
if age_type == 'edge'
|
50
|
+
self.end_id = response_hash['end_id']
|
51
|
+
self.start_id = response_hash['start_id']
|
52
|
+
# reload the nodes? (can we change the nodes?)
|
53
|
+
# self.end_node = ApacheAge::Entity.find(end_id)
|
54
|
+
# self.start_node = ApacheAge::Entity.find(start_id)
|
55
|
+
end
|
56
|
+
|
57
|
+
self
|
58
|
+
end
|
59
|
+
|
60
|
+
def destroy
|
61
|
+
match_clause = (age_type == 'vertex' ? "(done:#{age_label})" : "()-[done:#{age_label}]->()")
|
62
|
+
delete_clause = (age_type == 'vertex' ? 'DETACH DELETE done' : 'DELETE done')
|
63
|
+
cypher_sql =
|
64
|
+
<<-SQL
|
65
|
+
SELECT *
|
66
|
+
FROM cypher('#{age_graph}', $$
|
67
|
+
MATCH #{match_clause}
|
68
|
+
WHERE id(done) = #{id}
|
69
|
+
#{delete_clause}
|
70
|
+
return done
|
71
|
+
$$) as (deleted agtype);
|
72
|
+
SQL
|
73
|
+
|
74
|
+
hash = execute_sql(cypher_sql)
|
75
|
+
return nil if hash.blank?
|
76
|
+
|
77
|
+
self.id = nil
|
78
|
+
self
|
79
|
+
end
|
80
|
+
alias destroy! destroy
|
81
|
+
alias delete destroy
|
82
|
+
|
83
|
+
# private
|
84
|
+
|
85
|
+
def age_properties
|
86
|
+
attrs = attributes.except('id')
|
87
|
+
attrs = attrs.except('end_node', 'start_node', 'end_id', 'start_id') if age_type == 'edge'
|
88
|
+
attrs.symbolize_keys
|
89
|
+
end
|
90
|
+
|
91
|
+
def age_hash
|
92
|
+
hash =
|
93
|
+
{
|
94
|
+
id:,
|
95
|
+
label: age_label,
|
96
|
+
properties: age_properties
|
97
|
+
}
|
98
|
+
hash.merge!(end_id:, start_id:) if age_type == 'edge'
|
99
|
+
hash.transform_keys(&:to_s)
|
100
|
+
end
|
101
|
+
|
102
|
+
def properties_to_s
|
103
|
+
string_values =
|
104
|
+
age_properties.each_with_object([]) do |(key, val), array|
|
105
|
+
array << "#{key}: '#{val}'"
|
106
|
+
end
|
107
|
+
"{#{string_values.join(', ')}}"
|
108
|
+
end
|
109
|
+
|
110
|
+
def age_alias
|
111
|
+
return nil if id.blank?
|
112
|
+
|
113
|
+
# we start the alias with a since we can't start with a number
|
114
|
+
'a' + Digest::SHA256.hexdigest(id.to_s).to_i(16).to_s(36)[0..9]
|
115
|
+
end
|
116
|
+
|
117
|
+
def execute_sql(cypher_sql)
|
118
|
+
age_result = ActiveRecord::Base.connection.execute(cypher_sql)
|
119
|
+
age_type = age_result.values.first.first.split('::').last
|
120
|
+
json_data = age_result.values.first.first.split('::').first
|
121
|
+
# json_data = age_result.to_a.first.values.first.split("::#{age_type}").first
|
122
|
+
|
123
|
+
JSON.parse(json_data)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module ApacheAge
|
2
|
+
module Edge
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
include ActiveModel::Model
|
7
|
+
include ActiveModel::Dirty
|
8
|
+
include ActiveModel::Attributes
|
9
|
+
|
10
|
+
attribute :id, :integer
|
11
|
+
attribute :end_id, :integer
|
12
|
+
attribute :start_id, :integer
|
13
|
+
attribute :end_node # :vertex
|
14
|
+
attribute :start_node # :vertex
|
15
|
+
|
16
|
+
validates :end_node, :start_node, presence: true
|
17
|
+
|
18
|
+
extend ApacheAge::ClassMethods
|
19
|
+
include ApacheAge::CommonMethods
|
20
|
+
end
|
21
|
+
|
22
|
+
def age_type = 'edge'
|
23
|
+
|
24
|
+
# AgeSchema::Edges::WorksAt.create(
|
25
|
+
# start_node: fred, end_node: quarry, employee_role: 'Crane Operator'
|
26
|
+
# )
|
27
|
+
# SELECT *
|
28
|
+
# FROM cypher('age_schema', $$
|
29
|
+
# MATCH (start_vertex:Person), (end_vertex:Company)
|
30
|
+
# WHERE id(start_vertex) = 1125899906842634 and id(end_vertex) = 844424930131976
|
31
|
+
# CREATE (start_vertex)-[edge:WorksAt {employee_role: 'Crane Operator'}]->(end_vertex)
|
32
|
+
# RETURN edge
|
33
|
+
# $$) as (edge agtype);
|
34
|
+
def create_sql
|
35
|
+
self.start_node = start_node.save unless start_node.persisted?
|
36
|
+
self.end_node = end_node.save unless end_node.persisted?
|
37
|
+
<<-SQL
|
38
|
+
SELECT *
|
39
|
+
FROM cypher('#{age_graph}', $$
|
40
|
+
MATCH (from_node:#{start_node.age_label}), (to_node:#{end_node.age_label})
|
41
|
+
WHERE id(from_node) = #{start_node.id} and id(to_node) = #{end_node.id}
|
42
|
+
CREATE (from_node)-[edge#{self}]->(to_node)
|
43
|
+
RETURN edge
|
44
|
+
$$) as (edge agtype);
|
45
|
+
SQL
|
46
|
+
end
|
47
|
+
|
48
|
+
# So far just properties of string type with '' around them
|
49
|
+
def update_sql
|
50
|
+
alias_name = age_alias || age_label.downcase
|
51
|
+
set_caluse =
|
52
|
+
age_properties.map { |k, v| v ? "#{alias_name}.#{k} = '#{v}'" : "#{alias_name}.#{k} = NULL" }.join(', ')
|
53
|
+
<<-SQL
|
54
|
+
SELECT *
|
55
|
+
FROM cypher('#{age_graph}', $$
|
56
|
+
MATCH ()-[#{alias_name}:#{age_label}]->()
|
57
|
+
WHERE id(#{alias_name}) = #{id}
|
58
|
+
SET #{set_caluse}
|
59
|
+
RETURN #{alias_name}
|
60
|
+
$$) as (#{age_label} agtype);
|
61
|
+
SQL
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module ApacheAge
|
2
|
+
class Entity
|
3
|
+
class << self
|
4
|
+
def find_by(attributes)
|
5
|
+
where_clause = attributes.map { |k, v| "find.#{k} = '#{v}'" }.join(' AND ')
|
6
|
+
handle_find(where_clause)
|
7
|
+
end
|
8
|
+
|
9
|
+
def find(id)
|
10
|
+
where_clause = "id(find) = #{id}"
|
11
|
+
handle_find(where_clause)
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def age_graph = 'age_schema'
|
17
|
+
|
18
|
+
def handle_find(where_clause)
|
19
|
+
# try to find a vertex
|
20
|
+
match_node = '(find)'
|
21
|
+
cypher_sql = find_sql(match_node, where_clause)
|
22
|
+
age_response = execute_find(cypher_sql)
|
23
|
+
|
24
|
+
if age_response.nil?
|
25
|
+
# if not a vertex try to find an edge
|
26
|
+
match_edge = '()-[find]->()'
|
27
|
+
cypher_sql = find_sql(match_edge, where_clause)
|
28
|
+
age_response = execute_find(cypher_sql)
|
29
|
+
return nil if age_response.nil?
|
30
|
+
end
|
31
|
+
|
32
|
+
instantiate_result(age_response)
|
33
|
+
end
|
34
|
+
|
35
|
+
def execute_find(cypher_sql)
|
36
|
+
age_result = ActiveRecord::Base.connection.execute(cypher_sql)
|
37
|
+
return nil if age_result.values.first.nil?
|
38
|
+
|
39
|
+
age_result
|
40
|
+
end
|
41
|
+
|
42
|
+
def instantiate_result(age_response)
|
43
|
+
age_type = age_response.values.first.first.split('::').last
|
44
|
+
json_string = age_response.values.first.first.split('::').first
|
45
|
+
json_data = JSON.parse(json_string)
|
46
|
+
|
47
|
+
age_label = json_data['label']
|
48
|
+
attribs = json_data.except('label', 'properties')
|
49
|
+
.merge(json_data['properties'])
|
50
|
+
.symbolize_keys
|
51
|
+
|
52
|
+
"#{json_data['label'].gsub('__', '::')}".constantize.new(**attribs)
|
53
|
+
end
|
54
|
+
|
55
|
+
def find_sql(match_clause, where_clause)
|
56
|
+
<<-SQL
|
57
|
+
SELECT *
|
58
|
+
FROM cypher('#{age_graph}', $$
|
59
|
+
MATCH #{match_clause}
|
60
|
+
WHERE #{where_clause}
|
61
|
+
RETURN find
|
62
|
+
$$) as (found agtype);
|
63
|
+
SQL
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module ApacheAge
|
2
|
+
module Vertex
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
# include ApacheAge::Entity
|
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::ClassMethods
|
14
|
+
include ApacheAge::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
|
data/lib/rails_age.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require "rails_age/version"
|
2
|
+
require "rails_age/engine"
|
3
|
+
|
4
|
+
module RailsAge
|
5
|
+
# Your code goes here...
|
6
|
+
end
|
7
|
+
|
8
|
+
module ApacheAge
|
9
|
+
require "apache_age/class_methods"
|
10
|
+
require "apache_age/common_methods"
|
11
|
+
require "apache_age/edge"
|
12
|
+
require "apache_age/entity"
|
13
|
+
require "apache_age/vertex"
|
14
|
+
end
|
metadata
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rails_age
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Bill Tihen
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-05-21 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 7.1.3.2
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 7.1.3.2
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rspec-rails
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
description: Apache AGE plugin for Rails 7.1
|
42
|
+
email:
|
43
|
+
- btihen@gmail.com
|
44
|
+
executables: []
|
45
|
+
extensions: []
|
46
|
+
extra_rdoc_files: []
|
47
|
+
files:
|
48
|
+
- CHANGELOG.md
|
49
|
+
- MIT-LICENSE
|
50
|
+
- README.md
|
51
|
+
- Rakefile
|
52
|
+
- app/assets/config/rails_age_manifest.js
|
53
|
+
- app/assets/stylesheets/rails_age/application.css
|
54
|
+
- app/controllers/rails_age/application_controller.rb
|
55
|
+
- app/helpers/rails_age/application_helper.rb
|
56
|
+
- app/jobs/rails_age/application_job.rb
|
57
|
+
- app/mailers/rails_age/application_mailer.rb
|
58
|
+
- app/models/rails_age/application_record.rb
|
59
|
+
- app/views/layouts/rails_age/application.html.erb
|
60
|
+
- config/routes.rb
|
61
|
+
- db/migrate/20240521062349_configure_apache_age.rb
|
62
|
+
- db/schema.rb
|
63
|
+
- lib/apache_age/class_methods.rb
|
64
|
+
- lib/apache_age/common_methods.rb
|
65
|
+
- lib/apache_age/edge.rb
|
66
|
+
- lib/apache_age/entity.rb
|
67
|
+
- lib/apache_age/vertex.rb
|
68
|
+
- lib/rails_age.rb
|
69
|
+
- lib/rails_age/engine.rb
|
70
|
+
- lib/rails_age/version.rb
|
71
|
+
- lib/tasks/rails_age_tasks.rake
|
72
|
+
homepage: https://github.com/marpori/rails_age
|
73
|
+
licenses:
|
74
|
+
- MIT
|
75
|
+
metadata:
|
76
|
+
homepage_uri: https://github.com/marpori/rails_age
|
77
|
+
source_code_uri: https://github.com/marpori/rails_age
|
78
|
+
changelog_uri: https://github.com/marpori/rails_age/blob/main/CHANGELOG.md
|
79
|
+
post_install_message:
|
80
|
+
rdoc_options: []
|
81
|
+
require_paths:
|
82
|
+
- lib
|
83
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
84
|
+
requirements:
|
85
|
+
- - ">="
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '0'
|
88
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
89
|
+
requirements:
|
90
|
+
- - ">="
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: '0'
|
93
|
+
requirements: []
|
94
|
+
rubygems_version: 3.5.9
|
95
|
+
signing_key:
|
96
|
+
specification_version: 4
|
97
|
+
summary: Apache AGE plugin for Rails 7.1
|
98
|
+
test_files: []
|