rails_age 0.3.2 → 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dc953f22e786b1ce4280d8ced0bcb6ca768f89773c49a4ad0d7ddd3e3505e91e
4
- data.tar.gz: fff1d2809ce6f439196bfbc8a50b7990453d17def541829ef332ee2711a7fd3a
3
+ metadata.gz: 8228c60b43732a31b364b658e65b5d6a2e1b76018b573b1ec5b7fa25582f519c
4
+ data.tar.gz: 60f5211cb37aa6605523f76cde81158b6495448e90d4fbeabecd020c38520c4e
5
5
  SHA512:
6
- metadata.gz: e03cb391c29274953ab63badc3da19a9a965d53b23844086775d5668e61f32feba68605078b506c2a1ef8bde9d6627c67c087b7b1752acd2ccff681a55d4eec2
7
- data.tar.gz: 6373605ea72f6d85babde6ba167135f3098bb6b04c60a56015a04670e48f40e452a2473ce974de2e5577d2930d267aff72e253fc0a1fe10f740def2b28c0cf1b
6
+ metadata.gz: eec16e9c7b4512793b4aac9a3ac1c9ee0ce6280a1033712cc6aed5466218ffa17a47c1fca4d1f4d0508ce38e4a12a9a0c392d927f72a62c5ebcf8d2ea7c343f5
7
+ data.tar.gz: 3ccdeba21d32bc35de9162698c5c340efce5b0557b5930a3b1e0d9caaf5697f5afcd2b39ad65d0c976b5b3bc46df5edaf18a7bb96e023a2090642b08eb002786
data/CHANGELOG.md CHANGED
@@ -1,26 +1,51 @@
1
1
  # Change Log
2
2
 
3
- ## VERSION 0.4.0 - 2024-xx-xx
3
+ ## VERSION 0.5.0 - 2024-xx-xx
4
+
5
+ - **AGE Schema override** (instance and class methods) assumes db and migrations up-to-date
4
6
 
5
7
  - **cypher**
8
+ * schema override
6
9
  * query support
7
10
  * paths support
8
11
  * select attributes support
12
+
9
13
  - **Paths**
10
- * ?
11
14
 
12
- ## VERSION 0.3.4 - 2024-xx-xx
15
+ ## VERSION 0.4.4 - 2024-xx-xx
16
+
17
+ - **Edge Scaffold** (generates edge, type, view and controller)
18
+ * add `rails generate apache_age:edge_scaffold HasJob employee_role start_node:person end_node:company`
13
19
 
14
- - **Type Generators**
15
- * add `rails generate apache_age:type`
16
- * add `rails generate apache_age:node_type`
17
- * add `rails generate apache_age:edge_type`
20
+ ## VERSION 0.4.3 - 2024-xx-xx
18
21
 
19
- ## VERSION 0.3.3 - 2024-xx-xx
22
+ - **Node Scaffold** (generates node, type, view and controller)
23
+ * add `rails generate apache_age:node_scaffold Person first_name last_name age:integer`
24
+
25
+ ## VERSION 0.4.2 - 2024-xx-xx
20
26
 
21
27
  - **Edge Generator**
22
- * add `rails generate apache_age:edge` to create an edge model
28
+ * add `rails generate apache_age:edge HasPet owner_role` just a property
29
+ * add `rails generate apache_age:edge HasPet owner_role start_node:person end_node:pet`
30
+ with property and specified start-/end-nodes (person and pet nodes must have already been created)
31
+
32
+ ## VERSION 0.4.1 - 2024-xx-xx
23
33
 
34
+ - **OPTIONAL Installer**
35
+ * add `config_migrate` to `rails generate apache_age:install` auto fix the schema after `rails db:migrate`
36
+
37
+ ## VERSION 0.4.0 - 2024-06-14
38
+
39
+ Minor breaking change: type (:vertix) is now required in core for edges
40
+
41
+ - **Installer**
42
+ * AGE types added to installer (with tests)
43
+
44
+ - **Node Generator**
45
+ * add also creates node types (with tests)
46
+
47
+ - **Apache AGE Migrate**
48
+ * add `bin/rails apache_age:migrate` runs `bin/rails db:migrate` followed by `bin/rails apache_age:config_schema` to fix the schema file after `bin/rails db:migrate`
24
49
 
25
50
  ## VERSION 0.3.2 - 2024-06-08
26
51
 
@@ -33,6 +58,7 @@
33
58
 
34
59
  - **Installer**
35
60
  * refactor into multiple independent tasks with tests
61
+
36
62
  - **Documentation**
37
63
  * updated README with additional information
38
64
  * added `db/structure.sql` config to README
@@ -41,6 +67,7 @@
41
67
 
42
68
  - **Edges**
43
69
  * `find_by(start_node:, :end_node:, properties:)` to find an edge with specific nodes & properties (deprecated `find_edge`)
70
+
44
71
  - **Installer** (`rails generate apache_age:install`)
45
72
  * copy Age PG Extenstion migration to `db/migrate`
46
73
  * run the AGE PG Migration
@@ -56,6 +83,7 @@ NOTE: the `rails generate apache_age:install` can be run at any time to repair t
56
83
  * add missing methods to use in rails controllers
57
84
  * validate edge start- & end-nodes are valid
58
85
  * add unique edge validations
86
+
59
87
  - **Nodes**
60
88
  * add missing methods to use in rails controllers
61
89
  * add unique node validations
@@ -67,9 +95,11 @@ Initial release has the following features:
67
95
  - **Nodes:**
68
96
  * `.create`, `.read`, `.update`, `.delete`, `.all`, `.find(by id)`, `.find_by(age_properties)`
69
97
  * verified with usage in a controller and views
98
+
70
99
  - **Edges:**
71
100
  *`.create`, `.read`, `.update`, `.delete`, `.all`, `.find(by id)`, `.find_by(age_properties)`
72
101
  * verified with usage in a controller and views
102
+
73
103
  - **Entities:**
74
104
  * `.all`, `.find(id)`, `.find_by(age_property)` use these when class, label, edge, node
75
105
 
data/README.md CHANGED
@@ -12,371 +12,109 @@ Add this line to your application's Gemfile:
12
12
  gem "rails_age"
13
13
  ```
14
14
 
15
- ### Quick Install
15
+ ## Quick Start
16
16
 
17
17
  using the installer, creates the migration to install age, runs the migration, and adjusts the schema file, and updates the `config/database.yml` file.
18
18
 
19
+ setup (& Test) postgresql with AGE (using the docker version of AGE DB may be the easiest way to get started)
20
+ using the docker version of AGE DB, you can confirm psql AGE with the following commands:
19
21
  ```bash
20
- $ bundle
21
- $ bin/rails apache_age:install
22
- $ git add .
23
- $ git commit -m "Add Apache Age to Rails"
24
- $ rails generate apache_age:node Company company_name
25
- $ rails generate apache_age:node Person first_name last_name age:integer
22
+ psql -h localhost -p 5455 -U docker_username
23
+ > CREATE EXTENSION IF NOT EXISTS age;
24
+ > LOAD 'age';
25
+ > SET search_path = ag_catalog, "$user", public;
26
+ > SELECT create_graph('age_schema');
27
+ > \q
26
28
  ```
27
29
 
28
- NOTE: it is important to commit the `db/schema.rb` to git because `rails db:migrate` inappropriately modifies the schema file (I haven't yet tested `db/structure.sql`).
29
-
30
- **You can run `bin/rails apache_age:install` at any time to repair the schema file as needed.**
31
-
32
- For now, if you are using `db/structure.sql` you will need to manually configure Apache Age (RailsAge) as described below.
33
-
34
- ### Manual Install
35
-
36
- create a migration to add the Apache Age extension to your database
30
+ create a new Rails app (WITH POSTGRESQL!)
37
31
  ```bash
38
- $ bin/rails g migration AddApacheAge
32
+ rails new age_demo -d postgresql
33
+ cd age_demo
34
+ git add .
35
+ git commit -m "Initial Rails App"
39
36
  ```
40
- copy the contents of https://github.com/marpori/rails_age/blob/main/db/migrate/20240521062349_add_apache_age.rb
41
- ```ruby
42
- class AddApacheAge < ActiveRecord::Migration[7.1]
43
- def up
44
- # Allow age extension
45
- execute('CREATE EXTENSION IF NOT EXISTS age;')
46
-
47
- # Load the age code
48
- execute("LOAD 'age';")
49
-
50
- # Load the ag_catalog into the search path
51
- execute('SET search_path = ag_catalog, "$user", public;')
52
-
53
- # Create age_schema graph if it doesn't exist
54
- execute("SELECT create_graph('age_schema');")
55
- end
56
-
57
- def down
58
- execute <<-SQL
59
- DO $$
60
- BEGIN
61
- IF EXISTS (
62
- SELECT 1
63
- FROM pg_constraint
64
- WHERE conname = 'fk_graph_oid'
65
- ) THEN
66
- ALTER TABLE ag_catalog.ag_label
67
- DROP CONSTRAINT fk_graph_oid;
68
- END IF;
69
- END $$;
70
- SQL
71
-
72
- execute("SELECT drop_graph('age_schema', true);")
73
- execute('DROP SCHEMA IF EXISTS ag_catalog CASCADE;')
74
- execute('DROP EXTENSION IF EXISTS age;')
75
- end
76
- end
37
+ configure `config/database.yml` when using the docker version of AGE DB my config looks like:
38
+ ```yaml
39
+ port: 5455
40
+ host: localhost
41
+ username: docker_username
42
+ password: dockerized_password
77
43
  ```
78
- into your new migration file
79
44
 
80
- then run the migration
45
+ now you should be able to create the rails database:
81
46
  ```bash
82
- $ bin/rails db:migrate
83
- ```
84
-
85
- Rails migrate will mangle the schema `db/schema.rb` file. You need to remove the lines that look like:
86
- ```ruby
87
- ActiveRecord::Schema[7.1].define(version: 2024_05_21_062349) do
88
- create_schema "ag_catalog"
89
- create_schema "age_schema"
90
-
91
- # These are extensions that must be enabled in order to support this database
92
- enable_extension "age"
93
- enable_extension "plpgsql"
94
-
95
- # Could not dump table "_ag_label_edge" because of following StandardError
96
- # Unknown type 'graphid' for column 'id'
97
-
98
- # Could not dump table "_ag_label_vertex" because of following StandardError
99
- # Unknown type 'graphid' for column 'id'
100
-
101
- # Could not dump table "ag_graph" because of following StandardError
102
- # Unknown type 'regnamespace' for column 'namespace'
103
-
104
- # Could not dump table "ag_label" because of following StandardError
105
- # Unknown type 'regclass' for column 'relation'
106
-
107
- add_foreign_key "ag_label", "ag_graph", column: "graph", primary_key: "graphid", name: "fk_graph_oid"
108
-
109
- # other migrations
110
- # ...
111
- end
112
- ```
113
-
114
- and replace them with the following lines:
115
- ```ruby
116
- ActiveRecord::Schema[7.1].define(version: 2024_05_21_062349) do
117
- # These are extensions that must be enabled in order to support this database
118
- enable_extension 'plpgsql'
119
-
120
- # Allow age extension
121
- execute('CREATE EXTENSION IF NOT EXISTS age;')
122
-
123
- # Load the age code
124
- execute("LOAD 'age';")
125
-
126
- # Load the ag_catalog into the search path
127
- execute('SET search_path = ag_catalog, "$user", public;')
128
-
129
- # Create age_schema graph if it doesn't exist
130
- execute("SELECT create_graph('age_schema');")
131
-
132
- # other migrations
133
- # ...
134
- end
47
+ rails db:create
48
+ rails db:migrate
49
+ git add .
50
+ git commit -m "Add Apache Age Postgres DB configured with Rails App"
135
51
  ```
136
52
 
137
- NOTE: if using `db/structure.sql` use:
138
- ```sql
139
- -- These are extensions that must be enabled in order to support this database
140
- CREATE EXTENSION IF NOT EXISTS plpgsql WITH SCHEMA public;
141
-
142
- -- Allow age extension (if not already enabled), this builds the age_catalog schema
143
- CREATE EXTENSION IF NOT EXISTS age;
144
-
145
- -- Load the age module
146
- LOAD 'age';
147
-
148
- -- Load the ag_catalog into the search path
149
- SET search_path = ag_catalog, "$user", public;
150
-
151
- -- Create age_schema graph if it doesn't exist
152
- SELECT create_graph('age_schema');
153
-
154
- # other migrations
155
- # ...
156
-
157
- INSERT INTO "schema_migrations" (version) VALUES
158
- ('20110315075839'),
159
- --- ...
160
- ('20240521062349');
53
+ install Apache Age (you can ignore the `unknown OID` warnings)
54
+ ```bash
55
+ bundle add rails_age
56
+ bundle install
57
+ bin/rails apache_age:install
58
+ git add .
59
+ git commit -m "Add Apache Age to Rails"
161
60
  ```
162
61
 
163
- ## Contributing
164
-
165
- Create an merge request (with tests) and I will review it/merge it when ready.
166
-
167
- ## License
168
-
169
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
170
-
171
- ## Usage
172
-
173
- I suggest you creat a folder create a folder called `app/nodes` and `app/edges` to keep the code organized.
174
- I frequently use the `app/graphs` folder to keep all the graph related code together in a Module (as is done in the [rails age demo app](https://github.com/marpori/rails_age_demo_app))
175
-
176
- 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.
177
-
178
- A trival, but fully functional [rails age demo app](https://github.com/marpori/rails_age_demo_app), based on the Flintstones Commic, is available for reference.
179
-
180
- ### Nodes
181
-
182
- ```ruby
183
- # app/graphs/nodes/company.rb
184
- module Nodes
185
- class Company
186
- include ApacheAge::Entities::Vertex
187
-
188
- attribute :company_name, :string
189
-
190
- validates :company_name, presence: true
191
- validates_with(
192
- ApacheAge::Validators::UniqueVertexValidator,
193
- attributes: [:company_name]
194
- )
195
- end
196
- end
62
+ make some nodes :string is the default type
63
+ ```bash
64
+ rails generate apache_age:node Company company_name
65
+ rails generate apache_age:node Person first_name last_name
66
+ rails generate apache_age:node Pet pet_name:string age:integer
197
67
  ```
68
+ make some edges (`:vertex` is the default type) for start_node and end_node
69
+ ```bash
70
+ # when start node and end node are not specified they are of type `:vertex`
71
+ # this is generally not recommended - exept when very generic relationships are needed
72
+ rails generate apache_age:edge HasJob employee_role
198
73
 
199
- ```ruby
200
- # app/graphs/nodes/person.rb
201
- module Nodes
202
- class Person
203
- include ApacheAge::Entities::Vertex
204
-
205
- attribute :first_name, :string, default: nil
206
- attribute :last_name, :string, default: nil
207
- attribute :given_name, :string, default: nil
208
- attribute :nick_name, :string, default: nil
209
-
210
- validates :first_name, :last_name, :given_name, :nick_name,
211
- presence: true
212
-
213
- def initialize(**attributes)
214
- super
215
- # use unless present? since attributes when empty sets to "" by default
216
- self.nick_name = first_name unless nick_name.present?
217
- self.given_name = last_name unless given_name.present?
218
- end
219
- end
220
- end
74
+ # this is recommended - use explicit start_node and end_node types
75
+ rails generate apache_age:node HasPet start_node:person end_node:pet caretaker_role
221
76
  ```
222
77
 
223
- ### Edges
78
+ **NOTE:** the default `rails db:migrate` inappropriately modifies the schema file. This installer patches the migration to prevent this (however this might break on rails updates, etc). **You can run `bin/rails apache_age:install` at any time to repair the schema file as needed.**
224
79
 
225
- ```ruby
226
- # app/graphs/edges/has_job.rb
227
- module Edges
228
- class HasJob
229
- include ApacheAge::Entities::Edge
230
-
231
- attribute :employee_role, :string
232
- attribute :start_node, :person
233
- attribute :end_node, :company
234
-
235
- validates :employee_role, presence: true
236
- validate :validate_unique
237
- # or with a one-liner
238
- # validates_with(
239
- # ApacheAge::Validators::UniqueEdgeValidator,
240
- # attributes: %i[employee_role start_node end_node]
241
- # )
242
-
243
- private
244
-
245
- def validate_unique
246
- ApacheAge::Validators::UniqueEdgeValidator
247
- .new(attributes: %i[employee_role start_node end_node])
248
- .validate(self)
249
- end
250
- end
251
- end
252
- ```
80
+ For now, if you are using `db/structure.sql` you will need to manually configure Apache Age (RailsAge) as described below.
253
81
 
254
82
  ### Rails Console Usage
255
83
 
256
84
  ```ruby
257
- fred = Nodes::Person.create(first_name: 'Fredrick Jay', nick_name: 'Fred', last_name: 'Flintstone', gender: 'male')
258
- fred.to_h
259
-
260
- quarry = Nodes::Company.create(company_name: 'Bedrock Quarry')
261
- quarry.to_h
262
-
263
- job = Edges::HasJob.create(start_node: fred, end_node: quarry, employee_role: 'Crane Operator')
264
- job.to_h
265
- ```
266
-
267
- ### Update Routes
268
-
269
- ```ruby
270
- Rails.application.routes.draw do
271
- # mount is not needed with the engine
272
- # mount RailsAge::Engine => "/rails_age"
273
-
274
- # defines the route for the people controller
275
- resources :people
276
-
277
- # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.
278
- # Can be used by load balancers and uptime monitors to verify that the app is live.
279
- get 'up' => 'rails/health#show', as: :rails_health_check
280
-
281
- # Defines the root path route ("/")
282
- root 'people#index'
283
- end
284
- ```
285
-
286
- ### Types (Optional)
287
-
288
- ```ruby
289
- # spec/dummy/config/initializers/types.rb
290
- require 'apache_age/types/age_type_generator'
291
-
292
- Rails.application.config.to_prepare do
293
- # Ensure the files are loaded
294
- require_dependency 'nodes/company'
295
- require_dependency 'nodes/person'
296
-
297
- # Register the custom types
298
- ActiveModel::Type.register(:company, ApacheAge::Types::AgeTypeGenerator.create_type_for(Nodes::Company))
299
- ActiveModel::Type.register(:person, ApacheAge::Types::AgeTypeGenerator.create_type_for(Nodes::Person))
300
- end
301
- ```
302
-
303
- ### Controller Usage
304
-
305
- ```ruby
306
- # app/controllers/people_controller.rb
307
- class PeopleController < ApplicationController
308
- before_action :set_person, only: %i[show edit update destroy]
309
-
310
- # GET /people or /people.json
311
- def index
312
- @people = Nodes::Person.all
313
- end
314
-
315
- # GET /people/1 or /people/1.json
316
- def show; end
317
-
318
- # GET /people/new
319
- def new
320
- @person = Nodes::Person.new
321
- end
322
-
323
- # GET /people/1/edit
324
- def edit; end
325
-
326
- # POST /people or /people.json
327
- def create
328
- @person = Nodes::Person.new(**person_params)
329
- respond_to do |format|
330
- if @person.save
331
- format.html { redirect_to person_url(@person), notice: 'Person was successfully created.' }
332
- format.json { render :show, status: :created, location: @person }
333
- else
334
- format.html { render :new, status: :unprocessable_entity }
335
- format.json { render json: @person.errors, status: :unprocessable_entity }
336
- end
337
- end
338
- end
339
-
340
- # PATCH/PUT /people/1 or /people/1.json
341
- def update
342
- respond_to do |format|
343
- if @person.update(**person_params)
344
- format.html { redirect_to person_url(@person), notice: 'Person was successfully updated.' }
345
- format.json { render :show, status: :ok, location: @person }
346
- else
347
- format.html { render :edit, status: :unprocessable_entity }
348
- format.json { render json: @person.errors, status: :unprocessable_entity }
349
- end
350
- end
351
- end
352
-
353
- # DELETE /people/1 or /people/1.json
354
- def destroy
355
- @person.destroy!
356
-
357
- respond_to do |format|
358
- format.html { redirect_to people_url, notice: 'Person was successfully destroyed.' }
359
- format.json { head :no_content }
360
- end
361
- end
362
-
363
- private
364
-
365
- # Use callbacks to share common setup or constraints between actions.
366
- def set_person
367
- @person = Nodes::Person.find(params[:id])
368
- end
369
-
370
- # Only allow a list of trusted parameters through.
371
- def person_params
372
- # params.fetch(:person, {})
373
- params.require(:nodes_person).permit(:first_name, :last_name, :nick_name, :given_name, :gender)
374
- end
375
- end
376
- ```
377
-
378
- ### Views
379
-
380
- ```erb
381
-
382
- ```
85
+ bin/rails c
86
+
87
+ fred = Person.new(first_name: 'Fredrick Jay', last_name: 'Flintstone')
88
+ fred.valid?
89
+ fred.save
90
+ fred.to_h # should have an ID
91
+
92
+ # fails because of a missing required field (Property)
93
+ incomplete = Person.new(first_name: 'Fredrick Jay')
94
+ incomplete.valid?
95
+ incomplete.errors
96
+ incomplete.to_h
97
+
98
+ # fails because of uniqueness constraints
99
+ jay = Person.create(first_name: 'Fredrick Jay', last_name: 'Flintstone')
100
+ jay.to_h
101
+ => {:id=>nil, :first_name=>"Fredrick Jay", :last_name=>"Flintstone"}
102
+ jay.valid?
103
+ => false
104
+ jay.errors
105
+ => #<ActiveModel::Errors [#<ActiveModel::Error attribute=base, type=record not unique, options={}>, #<ActiveModel::Error attribute=first_name, type=property combination not unique, options={}>, #<ActiveModel::Error attribute=last_name, type=property combination not unique, options={}>]>
106
+ irb(main):008> jav.to_h
107
+ => {:id=>nil, :first_name=>"Fredrick Jay", :last_name=>"Flintstone"}
108
+
109
+ # .create is a shortcut for .new and .save
110
+ quarry = Company.create(company_name: 'Bedrock Quarry')
111
+ quarry.to_h # should have an ID
112
+
113
+ # create an edge (no generator yet)
114
+ job = HasJob.create(start_node: fred, end_node: quarry, employee_role: 'Crane Operator')
115
+ job.to_h # should have an ID
116
+ ```
117
+
118
+ ## Manual Install, Config and Usage
119
+
120
+ see [Manuel Installation, Configuration and Usage](MANUAL_INSTALL.md)
@@ -11,9 +11,9 @@ module ApacheAge
11
11
  attribute :id, :integer
12
12
  attribute :end_id, :integer
13
13
  attribute :start_id, :integer
14
- # allow user to optionally specify the class type (or not) thus not adding: `:vertex`
15
- attribute :end_node
16
- attribute :start_node
14
+ # type: `:vertex` can be overriden with a specific node type
15
+ attribute :end_node, :vertex
16
+ attribute :start_node, :vertex
17
17
 
18
18
  validates :end_node, :start_node, presence: true
19
19
  validate :validate_nodes
@@ -35,10 +35,10 @@ module ApacheAge
35
35
  query = record.class.find_by(edge_attribs.compact)
36
36
  return if query.blank? || (query.id == record.id)
37
37
 
38
- record.errors.add(:base, 'attribute combination not unique')
39
- record.errors.add(:end_node, 'attribute combination not unique')
40
- record.errors.add(:start_node, 'attribute combination not unique')
41
- attributes.each { record.errors.add(_1, 'attribute combination not unique') }
38
+ record.errors.add(:base, 'record not unique')
39
+ record.errors.add(:end_node, 'node combination not unique')
40
+ record.errors.add(:start_node, 'node combination not unique')
41
+ attributes.each { record.errors.add(_1, 'prpoerty combination not unique') }
42
42
  end
43
43
 
44
44
  private
@@ -20,8 +20,8 @@ module ApacheAge
20
20
  # if no match is found or if it finds itself, it's valid
21
21
  return if query.blank? || (query.id == record.id)
22
22
 
23
- record.errors.add(:base, 'attribute combination not unique')
24
- attributes.each { record.errors.add(_1, 'attribute combination not unique') }
23
+ record.errors.add(:base, 'record not unique')
24
+ attributes.each { record.errors.add(_1, 'property combination not unique') }
25
25
  end
26
26
  end
27
27
  end
@@ -14,11 +14,13 @@ module ApacheAge
14
14
 
15
15
  def create_node_file
16
16
  template "node.rb.tt", File.join(destination_root, "app/nodes", class_path, "#{file_name}.rb")
17
+ add_type_config
17
18
  end
18
19
 
19
20
  def destroy_node_file
20
21
  file_path = File.join(destination_root, "app/nodes", class_path, "#{file_name}.rb")
21
22
  File.delete(file_path) if File.exist?(file_path)
23
+ remove_type_config
22
24
  end
23
25
 
24
26
  def attributes_list
@@ -52,5 +54,41 @@ module ApacheAge
52
54
  "#{' ' * (parent_module.split('::').length - 1 - index)}end"
53
55
  end.join("\n") + "\n"
54
56
  end
57
+
58
+ def add_type_config
59
+ return unless File.exist?(types_config_file)
60
+
61
+ types_content = File.read(types_config_file)
62
+ types_content.sub!(/^end\s*$/, "#{new_type_content}end")
63
+ File.open(types_config_file, 'w') { |file| file.write(types_content) }
64
+ puts " modified: config/initializers/types.rb"
65
+ end
66
+
67
+ def remove_type_config
68
+ return unless File.exist?(types_config_file)
69
+
70
+ type_to_remove = new_type_content
71
+
72
+ types_content = File.read(types_config_file)
73
+ types_content.gsub!(type_to_remove, '')
74
+ File.open(types_config_file, 'w') { |file| file.write(types_content) }
75
+ end
76
+
77
+ def types_config_file = File.join(Rails.root, 'config/initializers/types.rb')
78
+
79
+ def new_type_content
80
+ file_path = [class_path, file_name].reject(&:blank?).join('/').downcase
81
+ node_namespace = class_path.map(&:capitalize).join('::')
82
+ node_class_name = file_name.split('_').map(&:capitalize).join
83
+ node_namespaced_class = [node_namespace, node_class_name].reject(&:blank?).join('::')
84
+ type_name = [class_path.join('_'), file_name].reject(&:blank?).join('_')
85
+ content =
86
+ <<-RUBY
87
+ require_dependency '#{file_path}'
88
+ ActiveModel::Type.register(
89
+ :#{type_name}, ApacheAge::Types::AgeTypeGenerator.create_type_for(#{node_namespaced_class})
90
+ )
91
+ RUBY
92
+ end
55
93
  end
56
94
  end
@@ -1,3 +1,3 @@
1
1
  module RailsAge
2
- VERSION = '0.3.2'
2
+ VERSION = '0.4.0'
3
3
  end
@@ -3,7 +3,7 @@
3
3
  #
4
4
  namespace :apache_age do
5
5
  desc "Ensure the database.yml file is properly configured for Apache Age"
6
- task :database_config => :environment do
6
+ task :config_database => :environment do
7
7
 
8
8
  db_config_file = File.expand_path("#{Rails.root}/config/database.yml", __FILE__)
9
9
 
@@ -0,0 +1,35 @@
1
+ # namespace :apache_age do
2
+ # desc "Ensure db:migrate is followed by apache_age:config_schema"
3
+ # task :config_migrate do
4
+ # bin_rails_path = File.expand_path('../../../bin/rails', __FILE__)
5
+ # migration_hook =
6
+ # <<-RUBY
7
+ # # ensure db:migrate is followed with: `Rake::Task["apache_age:config_schema"].invoke`
8
+ # # which repairs the schema.rb file after the migration mangles it
9
+ # require_relative '../config/boot'
10
+ # require 'rails/commands'
11
+
12
+ # if ARGV.first == 'db:migrate'
13
+ # require 'rake'
14
+ # Rails.application.load_tasks
15
+ # Rake::Task['db:migrate'].invoke
16
+ # Rake::Task["apache_age:config_schema"].invoke
17
+ # else
18
+ # Rake::Task['rails:commands'].invoke(*ARGV)
19
+ # end
20
+ # RUBY
21
+
22
+ # # Read the current content of the bin/rails file
23
+ # bin_rails_content = File.read(bin_rails_path)
24
+
25
+ # # Check if the migration hook is already present
26
+ # unless bin_rails_content.include?('Rake::Task["apache_age:config_schema"].invoke')
27
+ # # Append the migration hook to the end of the bin/rails file
28
+ # File.open(bin_rails_path, 'a') do |file|
29
+ # file.write(migration_hook)
30
+ # end
31
+ # puts "Migration hook added to bin/rails."
32
+ # else
33
+ # puts "Migration hook already present in bin/rails."
34
+ # end
35
+ # end
@@ -3,8 +3,7 @@
3
3
  #
4
4
  namespace :apache_age do
5
5
  desc "Copy migrations from rails_age to application and update schema"
6
- task :schema_config => :environment do
7
- # source_schema = File.expand_path('../../../db/schema.rb', __FILE__)
6
+ task :config_schema => :environment do
8
7
  destination_schema = File.expand_path("#{Rails.root}/db/schema.rb", __FILE__)
9
8
 
10
9
  unless File.exist?(destination_schema)
@@ -0,0 +1,85 @@
1
+ # lib/tasks/install.rake
2
+ # Usage: `rake apache_age:config_types`
3
+ #
4
+ namespace :apache_age do
5
+ desc "Install AGE types from rails_age into the rails initializers"
6
+ task :config_types => :environment do
7
+ types_file_path = File.expand_path("#{Rails.root}/config/initializers/types.rb", __FILE__)
8
+ required_file_path = "require 'apache_age/types/age_type_generator'"
9
+ required_file_content =
10
+ <<~RUBY
11
+ require 'apache_age/types/age_type_generator'
12
+ # AGE Type Definition Usage (edges/nodes):
13
+ # require_dependency 'nodes/company'
14
+ # ActiveModel::Type.register(
15
+ # :company, ApacheAge::Types::AgeTypeGenerator.create_type_for(Nodes::Company)
16
+ # )
17
+ RUBY
18
+ node_type_content =
19
+ <<-RUBY
20
+ require_dependency 'apache_age/entities/vertex'
21
+ ActiveModel::Type.register(
22
+ :vertex, ApacheAge::Types::AgeTypeGenerator.create_type_for(ApacheAge::Entities::Vertex)
23
+ )
24
+ RUBY
25
+ edge_type_content =
26
+ <<-RUBY
27
+ require_dependency 'apache_age/entities/edge'
28
+ ActiveModel::Type.register(
29
+ :edge, ApacheAge::Types::AgeTypeGenerator.create_type_for(ApacheAge::Entities::Edge)
30
+ )
31
+ RUBY
32
+
33
+ unless File.exist?(types_file_path)
34
+ source_content =
35
+ <<~RUBY
36
+ # config/initializers/types.rb
37
+
38
+ #{required_file_content}
39
+ Rails.application.config.to_prepare do
40
+ # Register AGE types
41
+ #{node_type_content}
42
+ #{edge_type_content}
43
+ end
44
+ RUBY
45
+ File.open(types_file_path, 'w') { |file| file.write(source_content) }
46
+ puts "config/initializers/types.rb file created with AGE base types"
47
+ else
48
+ destination_content = File.read(types_file_path)
49
+ original_content = destination_content.dup
50
+
51
+ unless destination_content.include?(required_file_path)
52
+ destination_content.sub!(
53
+ /^(\s*Rails\.application\.config\.to_prepare do\n)/,
54
+ "#{required_file_content}\n\\1"
55
+ )
56
+ end
57
+
58
+ unless destination_content.include?('# Register AGE types')
59
+ destination_content.sub!(
60
+ /^(\s*Rails\.application\.config\.to_prepare do\n)/,
61
+ "\\1 # Register AGE types\n"
62
+ )
63
+ end
64
+
65
+ unless destination_content.include?(edge_type_content)
66
+ destination_content.sub!(
67
+ /^(\s*Rails\.application\.config\.to_prepare do\n # Register AGE types\n)/,
68
+ "\\1#{edge_type_content}"
69
+ )
70
+ end
71
+
72
+ unless destination_content.include?(node_type_content)
73
+ destination_content.sub!(
74
+ /^(\s*Rails\.application\.config\.to_prepare do\n # Register AGE types\n)/,
75
+ "\\1#{node_type_content}"
76
+ )
77
+ end
78
+
79
+ if destination_content != original_content
80
+ File.open(types_file_path, 'w') { |file| file.write(destination_content) }
81
+ puts "modified: config/initializers/types.rb"
82
+ end
83
+ end
84
+ end
85
+ end
@@ -10,10 +10,17 @@ namespace :apache_age do
10
10
  # run any new migrations
11
11
  Rake::Task["db:migrate"].invoke
12
12
 
13
+ # ensure the config/database.yml file has the proper configurations
14
+ Rake::Task["apache_age:config_database"].invoke
15
+
13
16
  # adjust the schema file (unfortunately rails mangles the schema file)
14
- Rake::Task["apache_age:schema_config"].invoke
17
+ Rake::Task["apache_age:config_schema"].invoke
15
18
 
16
- # ensure the config/database.yml file has the proper configurations
17
- Rake::Task["apache_age:database_config"].invoke
19
+ # ensure the config/initializers/types.rb file has the base AGE Types
20
+ Rake::Task["apache_age:config_types"].invoke
21
+
22
+ # # ensure bin/rails db:migrate is always followed with
23
+ # # `Rake::Task["apache_age:config_schema"].invoke` to ensure the schema isn't mangled
24
+ # Rake::Task["apache_age:config_migrate"].invoke
18
25
  end
19
26
  end
@@ -0,0 +1,7 @@
1
+ namespace :apache_age do
2
+ desc "Ensure 'db:migrate' is followed by 'apache_age:config_schema' to repair 'schema.rb' after migrations"
3
+ task :migrate do
4
+ Rake::Task['db:migrate'].invoke
5
+ Rake::Task["apache_age:config_schema"].invoke
6
+ end
7
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_age
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.2
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bill Tihen
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-06-08 00:00:00.000000000 Z
11
+ date: 2024-06-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -85,11 +85,13 @@ files:
85
85
  - lib/rails_age.rb
86
86
  - lib/rails_age/engine.rb
87
87
  - lib/rails_age/version.rb
88
+ - lib/tasks/config_database.rake
89
+ - lib/tasks/config_migrate.rake
90
+ - lib/tasks/config_schema.rake
91
+ - lib/tasks/config_types.rake
88
92
  - lib/tasks/copy_migrations.rake
89
- - lib/tasks/database_config.rake
90
- - lib/tasks/install.original.rake
91
93
  - lib/tasks/install.rake
92
- - lib/tasks/schema_config.rake
94
+ - lib/tasks/migrate.rake
93
95
  homepage: https://github.com/marpori/rails_age
94
96
  licenses:
95
97
  - MIT
@@ -1,257 +0,0 @@
1
- # lib/tasks/install.rake
2
- # Usage: `rake apache_age:install`
3
- #
4
- namespace :apache_age do
5
- desc "Install & configure Apache Age within Rails (updates migrations, schema & database.yml)"
6
- task :install_old => :environment do
7
- source_schema = File.expand_path('../../../db/schema.rb', __FILE__)
8
- destination_schema = File.expand_path("#{Rails.root}/db/schema.rb", __FILE__)
9
- source_migrations = File.expand_path('../../../db/migrate', __FILE__)
10
- destination_migrations = File.expand_path("#{Rails.root}/db/migrate", __FILE__)
11
- # create the migrations folder if needed
12
- FileUtils.mkdir_p(destination_migrations) unless File.exist?(destination_migrations)
13
- original_migrations =
14
- Dir.glob("#{destination_migrations}/*.rb").map { |file| File.basename(file).sub(/^\d+/, '') }
15
-
16
- # # check if the schema is non-existent or blank (NEW) we need to know how to handle schema
17
- # is_schema_blank = !File.exist?(destination_schema) || blank_schema?(destination_schema)
18
- # puts "Schema is blank: #{is_schema_blank}"
19
-
20
- # ensure we have a schema file
21
- unless File.exist?(destination_schema)
22
- run_db_create
23
- run_db_migrate
24
- end
25
-
26
- # copy our migrations to the application (last_migration_version is nil if no migration necessary)
27
- # last_migration_version = copy_migrations
28
- Rake::Task["apache_age:copy_migrations"].invoke
29
-
30
- updated_migrations =
31
- Dir.glob("#{destination_migrations}/*.rb").map { |file| File.basename(file).sub(/^\d+/, '') }
32
-
33
- # # run our new migrations (unless we have not added any new migrations)
34
- # if original_migrations == updated_migrations
35
- # puts "no new migrations were copied, skipping migrations"
36
- # else
37
- # puts "added Apache Age migrations, running migrations"
38
- # run_db_migrate
39
- # end
40
- run_db_migrate
41
-
42
- # adjust the schema file (unfortunately rails mangles the schema file)
43
- # if is_schema_blank
44
- # puts "creating new schema..."
45
- # create_new_schema(last_migration_version, destination_schema, source_schema)
46
- # else
47
- # puts "updating existing schema..."
48
- # update_existing_schema(last_migration_version, destination_schema, source_schema)
49
- # end
50
- Rake::Task["apache_age:schema_config"].invoke
51
-
52
- # ensure the config/database.yml file has the proper configurations
53
- # update_database_yml
54
- Rake::Task["apache_age:database_config"].invoke
55
- end
56
-
57
- def run_db_create
58
- puts "Running db:create..."
59
- Rake::Task["db:create"].invoke
60
- end
61
-
62
- def run_db_migrate
63
- puts "Running db:migrate..."
64
- Rake::Task["db:migrate"].invoke
65
- end
66
-
67
- def copy_migrations
68
- migration_version = nil
69
-
70
- source = File.expand_path('../../../db/migrate', __FILE__)
71
- destination = File.expand_path("#{Rails.root}/db/migrate", __FILE__)
72
-
73
- FileUtils.mkdir_p(destination) unless File.exist?(destination)
74
- existing_migrations =
75
- Dir.glob("#{destination}/*.rb").map { |file| File.basename(file).sub(/^\d+/, '') }
76
-
77
- Dir.glob("#{source}/*.rb").each do |file|
78
- filename = File.basename(file)
79
- test_name = filename.sub(/^\d+/, '')
80
-
81
- if existing_migrations.include?(test_name)
82
- puts "Skipping #{filename}, it already exists"
83
- else
84
- migration_version = Time.now.utc.strftime("%Y_%m_%d_%H%M%S")
85
- file_version = migration_version.delete('_')
86
- new_filename = filename.sub(/^\d+/, file_version)
87
- destination_file = File.join(destination, new_filename)
88
- FileUtils.cp(file, destination_file)
89
- puts "Copied #{filename} to #{destination} as #{new_filename}"
90
- end
91
- end
92
- migration_version
93
- end
94
-
95
- def blank_schema?(destination_schema)
96
- return false unless File.exist?(destination_schema)
97
-
98
- content = File.read(destination_schema)
99
- content.include?('define(version: 0)') &&
100
- (content.include?("enable_extension 'plpgsql'") || content.include?('enable_extension "plpgsql"')) &&
101
- content.scan(/enable_extension/).size == 1
102
- end
103
-
104
- def schema_rails_version(destination_schema)
105
- if File.exist?(destination_schema)
106
- content = File.read(destination_schema)
107
- version_match = content.match(/ActiveRecord::Schema\[(.*?)\]/)
108
- return version_match[1] if version_match
109
- else
110
- full_version = Rails.version
111
- primary_secondary_version = full_version.split('.')[0..1].join('.')
112
- primary_secondary_version
113
- end
114
- end
115
-
116
- def create_new_schema(last_migration_version, destination_schema, source_schema)
117
- if File.exist?(source_schema) && File.exist?(destination_schema)
118
- rails_version = schema_rails_version(destination_schema)
119
- source_content = File.read(source_schema)
120
-
121
- # ensure we use the Rails version from the destination schema
122
- source_content.gsub!(
123
- /ActiveRecord::Schema\[\d+\.\d+\]/,
124
- "ActiveRecord::Schema[#{rails_version}]"
125
- )
126
- # ensure we use the last migration version (not the source schema version)
127
- source_content.gsub!(
128
- /define\(version: \d{4}(?:_\d{2}){2}(?:_\d{6})?\) do/,
129
- "define(version: #{last_migration_version}) do"
130
- )
131
-
132
- File.write(destination_schema, source_content)
133
- puts "Created new schema in #{destination_schema} with necessary extensions and configurations."
134
- else
135
- puts "local db/schema.rb file not found."
136
- end
137
- end
138
-
139
- def update_existing_schema(last_migration_version, destination_schema, source_schema)
140
- if File.exist?(source_schema) && File.exist?(destination_schema)
141
- rails_version = schema_rails_version(destination_schema)
142
- source_content = File.read(source_schema)
143
- new_content =
144
- source_content.gsub(
145
- /.*ActiveRecord::Schema\[\d+\.\d+\]\.define\(version: \d{4}(?:_\d{2}){2}(?:_\d{6})?\) do\n|\nend$/,
146
- ''
147
- )
148
-
149
- destination_content = File.read(destination_schema)
150
-
151
- # Remove unwanted schema statements
152
- destination_content.gsub!(%r{^.*?create_schema "ag_catalog".*?\n}, '')
153
- destination_content.gsub!(%r{^.*?create_schema "age_schema".*?\n}, '')
154
- destination_content.gsub!(%r{^.*?enable_extension "age".*?\n}, '')
155
- destination_content.gsub!(%r{^.*?enable_extension "plpgsql".*?\n}, '')
156
- destination_content.gsub!(%r{^.*?# Could not dump table "ag_graph" because of following StandardError.*?\n}, '')
157
- destination_content.gsub!(%r{^.*?# Unknown type 'regnamespace' for column 'namespace'.*?\n}, '')
158
- destination_content.gsub!(%r{^.*?# Could not dump table "ag_label" because of following StandardError.*?\n}, '')
159
- destination_content.gsub!(%r{^.*?# Unknown type 'regclass' for column 'relation'.*?\n}, '')
160
- destination_content.gsub!(%r{^.*?# Unknown type 'graphid' for column 'id'.*?\n}, '')
161
- destination_content.gsub!(
162
- %r{^.*?# Could not dump table "_ag_label_edge" because of following StandardError.*?\n}, ''
163
- )
164
- destination_content.gsub!(
165
- %r{^.*?# Could not dump table "_ag_label_vertex" because of following StandardError.*?\n}, ''
166
- )
167
- destination_content.gsub!(%r{^.*?# Could not dump table "ag_graph" because of following StandardError.*?\n}, '')
168
- destination_content.gsub!(%r{^.*?# Could not dump table "ag_label" because of following StandardError.*?\n}, '')
169
- destination_content.gsub!(%r{^.*?add_foreign_key "ag_label", "ag_graph".*?\n}, '')
170
-
171
- # add new wanted schema statements (at the top of the schema)
172
- unless destination_content.include?(%{execute("LOAD 'age';")}) &&
173
- destination_content.include?(%{enable_extension 'plpgsql'}) &&
174
- destination_content.include?(%{execute("SELECT create_graph('age_schema');")}) &&
175
- destination_content.include?(%{execute('CREATE EXTENSION IF NOT EXISTS age;')}) &&
176
- destination_content.include?(%{execute('SET search_path = ag_catalog, "$user", public;')})
177
- # if not all are found then remove any found
178
- destination_content.gsub!(%r{^.*?execute("LOAD 'age';")*?\n}, '')
179
- destination_content.gsub!(%r{^.*?enable_extension 'plpgsql'*?\n}, '')
180
- destination_content.gsub!(%r{^.*?execute("SELECT create_graph('age_schema');")*?\n}, '')
181
- destination_content.gsub!(%r{^.*?execute('CREATE EXTENSION IF NOT EXISTS age;')*?\n}, '')
182
- destination_content.gsub!(%r{^.*?execute('SET search_path = ag_catalog, "$user", public;')*?\n}, '')
183
- destination_content.gsub!(%r{^.*?# Allow age extension*?\n}, '')
184
- destination_content.gsub!(%r{^.*?# Load the ag_catalog into the search path*?\n}, '')
185
- destination_content.gsub!(%r{^.*?# Create age_schema graph if it doesn't exist*?\n}, '')
186
- destination_content.gsub!(%r{^.*?# These are extensions that must be enabled in order to support this database*?\n}, '')
187
-
188
- # add all of the correct settings back in
189
- destination_content.sub!(
190
- %r{(ActiveRecord::Schema\[\d+\.\d+\]\.define\(version: \d{4}(?:_\d{2}){2}(?:_\d{6})?\) do\n)},
191
- "\\1#{new_content}\n"
192
- )
193
- puts 'db/schema.rb has been updated with the necessary configuration.'
194
- else
195
- puts 'db/schema.rb has the necessary configuration, no adjustments necessary'
196
- end
197
-
198
- existing_version = destination_content.match(/define\(version: (\d{4}(?:_\d{2}){2}(?:_\d{6})?)\)/)[1].gsub('_', '')
199
- current_version = last_migration_version ? last_migration_version.gsub('_', '') : existing_version
200
-
201
- # ensure we use the last migration version (not the source schema version)
202
- if current_version.to_i > existing_version.to_i
203
- destination_content.gsub!(
204
- /define\(version: \d{4}(?:_\d{2}){2}(?:_\d{6})?\) do/,
205
- "define(version: #{last_migration_version}) do"
206
- )
207
- puts "Updated schema version to the migration version #{last_migration_version}"
208
- end
209
-
210
- File.write(destination_schema, destination_content)
211
- puts "Updated #{destination_schema} with necessary extensions and configurations."
212
- else
213
- puts "local db/schema.rb file not found."
214
- end
215
- end
216
-
217
- def update_database_yml
218
- db_config_file = File.expand_path("#{Rails.root}/config/database.yml", __FILE__)
219
-
220
- # Read the file
221
- lines = File.readlines(db_config_file)
222
-
223
- # any uncommented "schema_search_path:" lines?
224
- path_index = lines.find_index { |line| !line.include?('#') && line.include?('schema_search_path:') }
225
- default_start_index = lines.index { |line| line.strip.start_with?('default:') }
226
-
227
- # when it finds an existing schema_search_path, it updates it
228
- if path_index && lines[path_index].include?('ag_catalog,age_schema')
229
- puts "schema_search_path already set to ag_catalog,age_schema nothing to do."
230
- return
231
- elsif path_index
232
- key, val = lines[path_index].split(': ')
233
- # remove any unwanted characters
234
- val = val.gsub(/[ "\s\"\"'\n]/, '')
235
- lines[path_index] = "#{key}: ag_catalog,age_schema,#{val}\n"
236
- puts "add ag_catalog,age_schema to schema_search_path"
237
- elsif default_start_index
238
- puts "add ag_catalog,age_schema,public to schema_search_path in the default section of database.yml"
239
- sections_index = lines.map.with_index { |line, index| index if !line.start_with?(' ') }.compact.sort
240
-
241
- # find the start of the default section
242
- next_section_in_list = sections_index.index(default_start_index) + 1
243
-
244
- # find the end of the default section (before the next section starts)
245
- path_insert_index = sections_index[next_section_in_list]
246
-
247
- lines.insert(path_insert_index, " schema_search_path: ag_catalog,age_schema,public\n")
248
- else
249
- puts "didn't find a default section in database.yml, please add the following line:"
250
- puts " schema_search_path: ag_catalog,age_schema,public"
251
- puts "to the apprpriate section of your database.yml"
252
- end
253
-
254
- # Write the modified lines back to the file
255
- File.open(db_config_file, 'w') { |file| file.write(lines.join) }
256
- end
257
- end