active_tree 0.2.9

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7bf00b350061ffb40ff9a7db9cb0f10d06bea1a20eb82062ac66029d5fd1ad52
4
+ data.tar.gz: 6babe94e0d5f928b9bb3c14306850211c6ba4cd5660ee5cfebd6ace953e8be6a
5
+ SHA512:
6
+ metadata.gz: 40dce12132b74bc1b53bb52bc1281e76c953fe8a486613ff362f742b4a51e6847b05938bfd84ea9be56230edec6112543263ef3d52caab25837c3a9ed0d2c837
7
+ data.tar.gz: 9679c3a799b5472be0786085c060fa1bbfa6fa8ca160412797e1a5a79fe69b2db6137573ecf4095341d98948301077e285ef27c3f0e861f462a1c55b64356f05
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/Gemfile ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gemspec
6
+
7
+ gem "rake", "~> 13.0"
8
+ gem "rspec", "~> 3.0"
9
+
10
+ gem "activerecord", "~> 6.0"
11
+
12
+ gem "jwt"
13
+ gem "pg_ltree"
data/Gemfile.lock ADDED
@@ -0,0 +1,63 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ active_tree (0.2.9)
5
+ activerecord (~> 6.0)
6
+ jwt (~> 2.2.3)
7
+ pg_ltree (~> 1.1.8)
8
+
9
+ GEM
10
+ remote: https://rubygems.org/
11
+ specs:
12
+ activemodel (6.1.4.1)
13
+ activesupport (= 6.1.4.1)
14
+ activerecord (6.1.4.1)
15
+ activemodel (= 6.1.4.1)
16
+ activesupport (= 6.1.4.1)
17
+ activesupport (6.1.4.1)
18
+ concurrent-ruby (~> 1.0, >= 1.0.2)
19
+ i18n (>= 1.6, < 2)
20
+ minitest (>= 5.1)
21
+ tzinfo (~> 2.0)
22
+ zeitwerk (~> 2.3)
23
+ concurrent-ruby (1.1.9)
24
+ diff-lcs (1.4.4)
25
+ i18n (1.8.10)
26
+ concurrent-ruby (~> 1.0)
27
+ jwt (2.2.3)
28
+ minitest (5.14.4)
29
+ pg (1.2.3)
30
+ pg_ltree (1.1.8)
31
+ activerecord (>= 4.0.0, <= 7.0.0.rc1)
32
+ pg (>= 0.17.0, < 2)
33
+ rake (13.0.6)
34
+ rspec (3.10.0)
35
+ rspec-core (~> 3.10.0)
36
+ rspec-expectations (~> 3.10.0)
37
+ rspec-mocks (~> 3.10.0)
38
+ rspec-core (3.10.1)
39
+ rspec-support (~> 3.10.0)
40
+ rspec-expectations (3.10.1)
41
+ diff-lcs (>= 1.2.0, < 2.0)
42
+ rspec-support (~> 3.10.0)
43
+ rspec-mocks (3.10.2)
44
+ diff-lcs (>= 1.2.0, < 2.0)
45
+ rspec-support (~> 3.10.0)
46
+ rspec-support (3.10.2)
47
+ tzinfo (2.0.4)
48
+ concurrent-ruby (~> 1.0)
49
+ zeitwerk (2.4.2)
50
+
51
+ PLATFORMS
52
+ x86_64-linux
53
+
54
+ DEPENDENCIES
55
+ active_tree!
56
+ activerecord (~> 6.0)
57
+ jwt
58
+ pg_ltree
59
+ rake (~> 13.0)
60
+ rspec (~> 3.0)
61
+
62
+ BUNDLED WITH
63
+ 2.2.27
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021 CheckThisOut
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,112 @@
1
+ [![Tests](https://github.com/nicksterious/active_tree/actions/workflows/ci.yml/badge.svg)](https://github.com/nicksterious/active_tree/actions/workflows/ci.yml) [![Ruby Gem](https://github.com/nicksterious/active_tree/actions/workflows/rubygems.yml/badge.svg)](https://github.com/nicksterious/active_tree/actions/workflows/rubygems.yml) [![Gem Version](https://badge.fury.io/rb/active_tree.svg)](https://badge.fury.io/rb/active_tree)
2
+
3
+ # ActiveTree
4
+
5
+ Storing, processing and working with hierarchical data has always been challenging. A multitude of data models and implementations exist already but every one of them makes huge compromises or lacks functionality.
6
+
7
+ This gem implements a denormalized database model for tree data as well as several vectors for convenient querying.
8
+
9
+ ## Installation
10
+
11
+ This gem is at home within a Rails 6+ console-only, API or full fledged application on top of a Postgres database.
12
+
13
+ Add this line to your application's Gemfile:
14
+
15
+ ```ruby
16
+ gem 'active_tree'
17
+ ```
18
+
19
+ And then execute:
20
+
21
+ $ bundle install
22
+
23
+ Or install it yourself as:
24
+
25
+ $ gem install active_tree
26
+
27
+ Upon installing the gem you must run the install process in your Rails application's root:
28
+
29
+ $ rails g active_tree:install
30
+
31
+ This will generate a `config/active_tree.yml` which you may customize to your needs, an initializer and a migration file which you can also customize to add any database columns your models will require.
32
+
33
+ ## Usage
34
+
35
+ Include the ActiveTreeAble concern into one of your models which will own lifecycle trees (owner model):
36
+
37
+ ```ruby
38
+ class User < ApplicationRecord
39
+ include ActiveTree::ActiveTreeAble
40
+ # ...
41
+ end
42
+ ```
43
+
44
+ This will extend your model and enable the following functionality:
45
+
46
+ ```ruby
47
+ # query with ActiveRecord syntax
48
+ User.last.active_trees
49
+ User.find_by(name: "Acme").active_trees.select("product_name, sum(product_price) as total_price").group(:product_name)
50
+ User.last.active_trees.where(...)
51
+ User.last.active_trees.where(...).group(...)
52
+ User.last.active_trees.where(...).limit(...).offset(...)
53
+
54
+ # AR query with ltree syntax
55
+ User.last.active_trees.disabled.match_path("*.ProductName.*")
56
+ User.last.active_trees.match_path("*{5,10}.CategoryName.*.ProductName.*")
57
+ User.last.active_trees.active.match_path("*.CategoryName.*.SubcategoryName.*.ProductName.*").where( product_price: [100..150]).sum(:product_price)
58
+
59
+ ActiveTree::Model.match_path("Top.Category.*").products.match_path("*.Scooter*").where( product_price: [ 1000..10000 ]).average(:product_price)
60
+ ActiveTree::Model.where(name: "RC Airplane").products.match_path("*.Battery.*").sum(:product_price)
61
+ ActiveTree::Model.where(owner: User.last).match_path("*{10,20}.*Category.*")
62
+ ActiveTree::Model.match_path("*.Product.*")
63
+
64
+ # pg_ltree queries
65
+ User.last.active_trees.last.parent
66
+ ActiveTree::Model.match_path("*.Category.*").children
67
+
68
+ # pg_ltree combined with AR syntax
69
+ User.last.active_trees(type: "Sellable::Product").children.match_path("*.Battery").children
70
+
71
+ # all queries can be directed to a specific partition:
72
+ ActiveTree::Model.owned_by( owner_id ).match_path("*.Category.*").where(currency: "USD").products.sum(:product_price)
73
+
74
+ ```
75
+
76
+ To see what syntax to use for path traversal please check out the following resources:
77
+ * pg_ltree gem https://github.com/sjke/pg_ltree
78
+ * Postgres ltree extension documentation https://www.postgresql.org/docs/9.1/ltree.html
79
+
80
+ The ActiveTree gem is designed to be compatible with PostgREST. PostgREST is an amazing tool that generates a CRUD REST API for your Postgres database, read more about it here: https://postgrest.org
81
+
82
+ If the `create_postgrest_roles` setting is on each new owner will be assigned a Postgres role allowing them to access data within their partition using PostgREST. Your owner model will be extended with a `.generate_jwt` method you can use to generate the PostgREST authentication token.
83
+
84
+ ## Metadata
85
+
86
+ The `ActiveTree::Model` comes with a JSONB column where you can store random information structured in any required way. Subclasses of the Model can implement methods to store/retrieve JSONB data as well as validations.
87
+
88
+ The `ActiveTree::Metadata` model can store key-value pairs queryable through ActiveRecord simple queries or joins:
89
+
90
+ ```ruby
91
+ ActiveTree::Model.match_path("*.Battery.*").metadata.where(key: "Shipping weight").sum(:value)
92
+ ```
93
+
94
+ ## Caveats
95
+
96
+ `pg_ltree` .child / .parent queries do not work across different models due to an ActiveRecord limitation that requires results to be related via inheritance.
97
+
98
+ ## Roadmap
99
+
100
+ * postgres RECURSIVE queries
101
+ * builders
102
+ * seeds/fixtures
103
+
104
+
105
+ ## Contributing
106
+
107
+ Bug reports, pull requests and feature suggestions are welcome on GitHub at https://github.com/nicksterious/active_tree
108
+
109
+ ## License
110
+
111
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
112
+
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "lca"
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require "irb"
15
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,6 @@
1
+ # we include the concern manually so there should be no need for this anymore
2
+ # besides, no need to polute other classes with our stuff.
3
+
4
+ # ActiveSupport.on_load(:active_record) do
5
+ # extend ActiveTree::Lcable
6
+ # end
@@ -0,0 +1,3 @@
1
+ class ActiveTree::Builder
2
+
3
+ end
@@ -0,0 +1,80 @@
1
+ require "jwt"
2
+
3
+ # read https://www.postgresqltutorial.com/postgresql-schema/
4
+
5
+ module ActiveTree
6
+ module ActiveTreeAble
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+
11
+ has_many :active_trees, class_name: "::ActiveTree::Model", foreign_key: :owner_id, as: :owner
12
+ after_create :active_tree_create_storage
13
+ before_destroy :active_tree_delete_storage
14
+
15
+ # instance methods
16
+ def active_tree_role
17
+ "active_tree_owner_#{id}_#{ ACTIVE_TREE_OPTIONS[:owner_role_suffix] }"
18
+ end # role
19
+
20
+ # Generates a JWT token the client (SPA) can pass to PostgREST for privilege escalation
21
+ def generate_jwt
22
+ payload = { role: self.active_tree_role }
23
+ ::JWT.encode payload, ACTIVE_TREE_OPTIONS[:jwt_secret], ACTIVE_TREE_OPTIONS[:jwt_encryption]
24
+ end # .generate_jwt
25
+
26
+
27
+ # Returns the LCA table name as configured within config/active_tree.yml
28
+ def active_tree_table_name
29
+ ACTIVE_TREE_OPTIONS[:table_name]
30
+ end # .active_tree_table_name
31
+
32
+
33
+ # Creates LCA table partition and role for owner
34
+ def active_tree_create_storage
35
+
36
+ # create data partition
37
+ active_tree_sql "create table if not exists #{active_tree_table_name}_#{id} partition of #{active_tree_table_name} for values in (#{id})"
38
+ # TODO create partition indexes
39
+
40
+ if ACTIVE_TREE_OPTIONS[:create_postgrest_roles]
41
+ # drop role if it exists
42
+ active_tree_sql "drop role if exists #{ active_tree_role }"
43
+
44
+ # create role
45
+ active_tree_sql "create role #{ active_tree_role }"
46
+
47
+ # grant privs
48
+ active_tree_sql "grant all privileges on #{active_tree_table_name}_#{id} to #{ active_tree_role }"
49
+ end
50
+
51
+ end # create_storage
52
+
53
+
54
+ # Deletes or detaches the partition and removes the role for this owner
55
+ def active_tree_delete_storage
56
+
57
+ if ACTIVE_TREE_OPTIONS[:create_postgrest_roles]
58
+ # revoke privs
59
+ active_tree_sql "REVOKE ALL PRIVILEGES ON #{active_tree_table_name}_#{id} FROM #{ active_tree_role }"
60
+
61
+ # delete role
62
+ active_tree_sql "drop role #{ active_tree_role }"
63
+ end
64
+
65
+ if ACTIVE_TREE_OPTIONS[:destroy_partition_on_owner_destroy]
66
+ # delete partition
67
+ active_tree_sql "drop table if exists #{active_tree_table_name}_#{id}"
68
+ else
69
+ # detach and forget about it
70
+ active_tree_sql "alter table #{active_tree_table_name} detach partition #{ active_tree_role }"
71
+ end
72
+ end # delete_storage
73
+
74
+ def active_tree_sql sql
75
+ ActiveRecord::Base.connection.execute sql
76
+ end # active_tree_sql
77
+
78
+ end # ClassMethods
79
+ end
80
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+ module ActiveTree::Statusable
3
+
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ scope :active, -> { where(status: 1) }
8
+ scope :inactive, -> { where(status: 0) }
9
+ alias_method :enabled?, :active?
10
+ alias_method :enable!, :active!
11
+ alias_method :on!, :active!
12
+ alias_method :disabled?, :inactive?
13
+ alias_method :disable!, :inactive!
14
+ alias_method :off!, :inactive!
15
+ alias_method :toggle?, :toggle_status!
16
+
17
+ before_create :set_default_status
18
+ end
19
+
20
+ def set_default_status
21
+ self.status ||= 1
22
+ end
23
+
24
+ def toggle_status!
25
+ if active?
26
+ inactive!
27
+ else
28
+ active!
29
+ end
30
+ end
31
+
32
+ def status?
33
+ [:inactive, :active][ status ]
34
+ end
35
+
36
+ def active?
37
+ status == 1
38
+ end
39
+ def inactive?
40
+ status == 0
41
+ end
42
+
43
+ def active!
44
+ self.update(status: 1)
45
+ end
46
+ def inactive!
47
+ self.update(status: 0)
48
+ end
49
+ end
@@ -0,0 +1,12 @@
1
+ class ActiveTree::Metadata < ActiveRecord::Base
2
+
3
+ belongs_to :model, polymorphic: true, required: true
4
+
5
+ def self.table_name
6
+ return "#{ ::ACTIVE_TREE_OPTIONS[:table_name] }_metadata" if defined? ::LCA_OPTIONS
7
+ return "active_tree_models_metadata"
8
+ end
9
+
10
+ validates_presence_of :key
11
+
12
+ end
@@ -0,0 +1,75 @@
1
+ class ActiveTree::Model < ActiveRecord::Base
2
+
3
+ include ActiveTree::Statusable
4
+
5
+ self.primary_key = :id
6
+
7
+ ltree :path
8
+
9
+ def self.table_name
10
+ return ::ACTIVE_TREE_OPTIONS[:table_name] if defined? ::ACTIVE_TREE_OPTIONS
11
+ return "active_tree_models"
12
+ end
13
+
14
+ belongs_to :owner, polymorphic: :true, required: true
15
+
16
+ scope :match_path, -> (some_path) { where("path ~ ?", "#{some_path}") }
17
+
18
+ validates_presence_of :name, allow_blank: false
19
+ validates_presence_of :path, allow_blank: false
20
+
21
+ has_many :metadata, class_name: "::ActiveTree::Metadata", dependent: :destroy, as: :model
22
+
23
+ before_validation :set_defaults
24
+ def set_defaults
25
+ self.path ||= name.delete(" ").gsub(/[^0-9a-z ]/i, '') if name
26
+ self.path_slug = path.parameterize if path
27
+ self.metadata_inline ||= {}
28
+ end
29
+
30
+
31
+ # Scoping by owner in order to select the partition
32
+ #
33
+ # @param owner_id [Integer] the partition owner
34
+ def self.owned_by(owner_id)
35
+ # if we're looking for anything else but an integer, revert to the base class
36
+ return self if !owner_id.is_a? Integer
37
+
38
+ partition_suffix = "_#{owner_id}"
39
+
40
+ table = "#{ self.table_name }#{ partition_suffix }"
41
+
42
+ ApplicationRecord.connection.schema_cache.clear!
43
+ return self if !ApplicationRecord.connection.schema_cache.data_source_exists? table
44
+
45
+ # duplicate the class
46
+ model_class = Class.new self
47
+ original_class_name = self.name
48
+
49
+ # ...for this owner
50
+ class_name = "#{name}#{partition_suffix}"
51
+
52
+ # specify the table
53
+ model_class.define_singleton_method(:table_name) do
54
+ table
55
+ end
56
+
57
+ # specify the name
58
+ model_class.define_singleton_method(:name) do
59
+ class_name
60
+ end
61
+
62
+ model_class.define_singleton_method(:sti_name) do
63
+ original_class_name
64
+ end
65
+
66
+ # override the STI name lmfao
67
+ model_class.define_singleton_method(:find_sti_class) do |p|
68
+ original_class_name.constantize
69
+ end
70
+
71
+ # proceed
72
+ model_class
73
+ end # .owned_by
74
+
75
+ end
@@ -0,0 +1,9 @@
1
+ class ActiveTree::Query
2
+ def valid?(attribute, value)
3
+ return false if !attribute.is_a? Symbol
4
+ return false if value.nil? || value.empty? || !value.present?
5
+ return valse if [ "", [], ["0"], [ 0 ], "all", "any" ].include? value
6
+ return true
7
+ end # valid?
8
+
9
+ end
@@ -0,0 +1,31 @@
1
+ class ActiveTree::ModelQuery < ActiveTree::Query
2
+ attr_accessor :initial_scope
3
+
4
+ def initialize(initial_scope = ::ActiveTree::Model.all)
5
+ @initial_scope = initial_scope
6
+ end # initialize
7
+
8
+ def call(params)
9
+ scope = simple_search(initial_scope, :id, params[:id])
10
+ [:owner_type, :owner_id, :status, :data_external_id, :data_provider, :type, :parent_entity_id, :parent_entity_type, :path_slug].each do |query|
11
+ scope = simple_search(scope, query, params[query])
12
+ end
13
+
14
+ scope = by_search(scope, params[:search])
15
+
16
+ scope
17
+ end # call
18
+
19
+
20
+ # Partial search by name
21
+ def by_search(scope, search = nil)
22
+ search ? scope.where("lower(name) like ?", "%#{search.downcase}%") : scope
23
+ end # by_search
24
+
25
+
26
+ # Simple search by attribute and exact value
27
+ def simple_search(scope, attribute = nil, value = nil)
28
+ valid?(attribute, value) ? scope.where(attribute => value) : scope
29
+ end # simple search
30
+
31
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveTree
4
+ VERSION = "0.2.9"
5
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+ require "active_support/all"
3
+
4
+ require "active_record"
5
+
6
+ require "pg_ltree"
7
+
8
+ require_relative "active_tree/version"
9
+
10
+ require_relative "active_tree/models/concerns/statusable"
11
+ require_relative "active_tree/models/metadata"
12
+ require_relative "active_tree/models/model"
13
+
14
+ require_relative "active_tree/models/concerns/active_tree_able"
15
+ require_relative "active_tree/active_record"
16
+
17
+ # TODO add query objects and builders
18
+ require_relative "active_tree/queries/active_tree_query"
19
+ require_relative "active_tree/queries/model_query"
20
+ #require_relative "active_tree/builders/"
21
+
22
+ module ActiveTree
23
+ class Error < StandardError; end
24
+
25
+ # Your code goes here...
26
+
27
+ class << self
28
+ attr_accessor :active_tree_models
29
+ attr_accessor :options
30
+ end
31
+
32
+ self.active_tree_models = []
33
+
34
+ def self.active_tree_options
35
+ @options ||= begin
36
+ path = Rails.root.join("config", "active_tree.yml").to_s
37
+ if File.exist?(path)
38
+ YAML.load(ERB.new(File.read(path)).result)
39
+ else
40
+ {
41
+ table_name: "active_tree_models",
42
+ jwt_secret: "",
43
+ jwt_encryption: "HS256"
44
+ }
45
+ end
46
+ end
47
+ end
48
+
49
+ def self.env
50
+ @env ||= ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
51
+ end
52
+ end
@@ -0,0 +1,29 @@
1
+ require "rails/generators/active_record"
2
+
3
+ module ActiveTree
4
+ module Generators
5
+ class InstallGenerator < Rails::Generators::Base
6
+ include ActiveRecord::Generators::Migration
7
+ source_root File.join(__dir__, "templates")
8
+
9
+ def copy_migration
10
+ migration_template "migration.rb.tt", "db/migrate/install_active_tree.rb", migration_version: migration_version
11
+ end
12
+
13
+ def copy_initializer
14
+ copy_file 'initializer.rb.tt', 'config/initializers/active_tree.rb'
15
+ end
16
+
17
+ def copy_config
18
+ conf_file = "config/active_tree.yml"
19
+ copy_file "config.yml.tt", conf_file
20
+ contents = File.read( conf_file ).gsub("changeme", ('a'..'z').to_a.shuffle.first(4).join )
21
+ File.open(conf_file, 'wb') { |file| file.write(contents) }
22
+ end
23
+
24
+ def migration_version
25
+ "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,18 @@
1
+ # the main table will be called "active_tree_models"
2
+ # partitions will be called "active_tree_models_X" where X is the owner object ID
3
+ table_name: "active_tree_models"
4
+
5
+ # create PG roles for postgrest: anon and owner-specific roles
6
+ create_postgrest_roles: true
7
+
8
+ # on owner removal, detach the partition but preserve the table and data, or destroy the partition and data
9
+ # if you choose to detach and preserve, in order to avoid table name collisions, make sure owner IDs are not reused
10
+ destroy_partition_on_owner_destroy: true
11
+
12
+ # jwt secret required for postgrest role switching
13
+ jwt_secret: "supersecret"
14
+ jwt_encryption: "HS256"
15
+
16
+ # suffix postgres roles with a random string
17
+ # to avoid collisions between other LCA installations in other apps using same db server
18
+ owner_role_suffix: "changeme"
@@ -0,0 +1,17 @@
1
+ ACTIVE_TREE_OPTIONS ||= begin
2
+ path = Rails.root.join("config", "active_tree.yml").to_s
3
+ if File.exist?(path)
4
+ YAML.load( ERB.new(File.read(path)).result ).deep_symbolize_keys
5
+ else
6
+ {
7
+ table_name: "active_tree_models",
8
+ create_postgrest_roles: true,
9
+ jwt_secret: "supersecret",
10
+ jwt_encryption: "HS256",
11
+ destroy_partition_on_owner_destroy: true,
12
+ owner_role_suffix: "changeme"
13
+ }
14
+ end
15
+ end.merge({
16
+ database_user: Rails.application.config.database_configuration[ Rails.env ].deep_symbolize_keys[:username]
17
+ })
@@ -0,0 +1,82 @@
1
+ class <%= migration_class_name %> < ActiveRecord::Migration<%= migration_version %>
2
+ def change
3
+
4
+ # enable ltree extension
5
+ begin
6
+ execute "create extension ltree"
7
+ rescue
8
+ p "LTREE was already enabled"
9
+ end
10
+
11
+
12
+ # create the main table as set up within ACTIVE_TREE_OPTIONS[:table_name]
13
+ execute <<-SQL
14
+ create table #{ ACTIVE_TREE_OPTIONS[:table_name] } (
15
+ id serial,
16
+
17
+ owner_id integer,
18
+ owner_type character varying,
19
+
20
+ status integer,
21
+
22
+ data_external_id character varying,
23
+ data_provider character varying,
24
+
25
+ type character varying,
26
+ name text,
27
+
28
+ parent_entity_id integer,
29
+ parent_entity_type character varying,
30
+
31
+ path ltree,
32
+ path_slug text,
33
+
34
+ metadata_inline jsonb,
35
+
36
+ created_at timestamp(6) without time zone not null,
37
+ updated_at timestamp(6) without time zone not null,
38
+
39
+ primary key (id, owner_id)
40
+ ) partition by list(owner_id)
41
+ SQL
42
+
43
+ # create an "others" partition for when the owner is undefined/unknown? just in case / may help in some edge cases
44
+ execute "CREATE TABLE #{ACTIVE_TREE_OPTIONS[:table_name]}_others PARTITION OF #{ACTIVE_TREE_OPTIONS[:table_name]} DEFAULT"
45
+
46
+
47
+ # add indexes
48
+ add_index ACTIVE_TREE_OPTIONS[:table_name], :id
49
+ add_index ACTIVE_TREE_OPTIONS[:table_name], :owner_id
50
+ add_index ACTIVE_TREE_OPTIONS[:table_name], :type
51
+ add_index ACTIVE_TREE_OPTIONS[:table_name], :parent_entity_id
52
+
53
+ # next two indexes unfortunately can't be unique since a cycle can appear several times under an owner
54
+ add_index ACTIVE_TREE_OPTIONS[:table_name], :path, using: :gist
55
+ add_index ACTIVE_TREE_OPTIONS[:table_name], [:data_provider, :data_external_id]
56
+
57
+
58
+ # when postgrest is enabled...
59
+ if ACTIVE_TREE_OPTIONS[:create_postgrest_roles]
60
+ # create postgrest anon user with no privs
61
+ # postgrest may pass an user's role using JWT
62
+ execute "drop role if exists postgrest_anon"
63
+ execute "create role postgrest_anon nologin"
64
+ execute "grant postgrest_anon to #{ ACTIVE_TREE_OPTIONS[:database_user] }"
65
+ end
66
+
67
+
68
+
69
+
70
+ # metadata table
71
+ create_table "#{ ACTIVE_TREE_OPTIONS[:table_name] }_metadata" do |t|
72
+ t.string :model_type
73
+ t.integer :model_id
74
+ t.string :key
75
+ t.text :value
76
+
77
+ t.timestamps
78
+ end
79
+ add_index "#{ ACTIVE_TREE_OPTIONS[:table_name] }_metadata", [ :model_type, :model_id ]
80
+ add_index "#{ ACTIVE_TREE_OPTIONS[:table_name] }_metadata", [ :key ]
81
+ end
82
+ end
metadata ADDED
@@ -0,0 +1,111 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: active_tree
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.9
5
+ platform: ruby
6
+ authors:
7
+ - Nick @ Earthster
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2021-10-08 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '6.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '6.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: pg_ltree
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 1.1.8
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 1.1.8
41
+ - !ruby/object:Gem::Dependency
42
+ name: jwt
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 2.2.3
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 2.2.3
55
+ description: This gem allows storing and querying tree data in a meaningful, comprehensive
56
+ way using a Postgres ltree structure
57
+ email:
58
+ - nick@earthster.org
59
+ executables: []
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - ".rspec"
64
+ - Gemfile
65
+ - Gemfile.lock
66
+ - LICENSE
67
+ - README.md
68
+ - Rakefile
69
+ - bin/console
70
+ - bin/setup
71
+ - lib/active_tree.rb
72
+ - lib/active_tree/active_record.rb
73
+ - lib/active_tree/builders/builder.rb
74
+ - lib/active_tree/models/concerns/active_tree_able.rb
75
+ - lib/active_tree/models/concerns/statusable.rb
76
+ - lib/active_tree/models/metadata.rb
77
+ - lib/active_tree/models/model.rb
78
+ - lib/active_tree/queries/active_tree_query.rb
79
+ - lib/active_tree/queries/model_query.rb
80
+ - lib/active_tree/version.rb
81
+ - lib/generators/active_tree/install_generator.rb
82
+ - lib/generators/active_tree/templates/config.yml.tt
83
+ - lib/generators/active_tree/templates/initializer.rb.tt
84
+ - lib/generators/active_tree/templates/migration.rb.tt
85
+ homepage: https://github.com/nicksterious/active_tree
86
+ licenses:
87
+ - MIT
88
+ metadata:
89
+ homepage_uri: https://github.com/nicksterious/active_tree
90
+ source_code_uri: https://github.com/nicksterious/active_tree
91
+ changelog_uri: https://github.com/nicksterious/active_tree/blob/master/CHANGELOG.md
92
+ post_install_message:
93
+ rdoc_options: []
94
+ require_paths:
95
+ - lib
96
+ required_ruby_version: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ version: 2.4.0
101
+ required_rubygems_version: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ version: '0'
106
+ requirements: []
107
+ rubygems_version: 3.0.3.1
108
+ signing_key:
109
+ specification_version: 4
110
+ summary: A partitioned storage backend for tree data
111
+ test_files: []