rails_age 0.1.0
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 +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: []
|