adjustable_schema 0.8.0 → 0.9.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 +4 -4
- data/CHANGELOG.md +16 -0
- data/Rakefile +7 -5
- data/app/models/adjustable_schema/application_record.rb +5 -3
- data/app/models/adjustable_schema/relationship/role.rb +8 -1
- data/app/models/adjustable_schema/relationship.rb +37 -19
- data/app/models/concerns/adjustable_schema/active_record/associations.rb +3 -1
- data/app/models/concerns/adjustable_schema/active_record/relationships.rb +9 -7
- data/config/initializers/associations.rb +2 -0
- data/config/initializers/model_names.rb +2 -0
- data/db/migrate/01_create_adjustable_schema_relationship_tables.rb +3 -1
- data/lib/adjustable_schema/active_record/association/hierarchy.rb +33 -0
- data/lib/adjustable_schema/active_record/association/naming.rb +31 -26
- data/lib/adjustable_schema/active_record/association/roleless.rb +27 -0
- data/lib/adjustable_schema/active_record/association/scopes.rb +30 -27
- data/lib/adjustable_schema/active_record/association.rb +58 -61
- data/lib/adjustable_schema/active_record.rb +3 -1
- data/lib/adjustable_schema/authors.rb +3 -1
- data/lib/adjustable_schema/config.rb +8 -9
- data/lib/adjustable_schema/engine.rb +3 -1
- data/lib/adjustable_schema/version.rb +3 -1
- data/lib/adjustable_schema.rb +5 -3
- data/lib/tasks/adjustable_schema_tasks.rake +2 -0
- metadata +9 -16
- data/app/assets/config/adjustable_schema_manifest.js +0 -1
- data/app/assets/stylesheets/adjustable_schema/application.css +0 -15
- data/app/controllers/adjustable_schema/application_controller.rb +0 -4
- data/app/helpers/adjustable_schema/application_helper.rb +0 -4
- data/app/jobs/adjustable_schema/application_job.rb +0 -4
- data/app/mailers/adjustable_schema/application_mailer.rb +0 -6
- data/app/views/layouts/adjustable_schema/application.html.erb +0 -15
- data/config/routes.rb +0 -2
- data/lib/adjustable_schema/active_record/query_methods.rb +0 -103
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3b1390e4f564f54d0e8a840fae882bf7a494b51323846ab06e05a786c332fc37
|
4
|
+
data.tar.gz: 5458b1a98b4b3a3dea0c3a8e2cd66e42003683cbbd4af95c939af8fe1810ce34
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: dee15ab2468fca2b6bf3739df1b65508dd80ae87830833d27895e05a21c59392e3856ffbd78742cc0c067bfd1db66b364a880f0bf5401a3a3cc5cac2879ec59e
|
7
|
+
data.tar.gz: 984449dfcba1f1797830b5153439d2873187b0fe32aaefe23cb21bab76b49f49906f70dc562eacb92653a531eb828c262449bf41a71b4507782de25ac38214d0
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,19 @@
|
|
1
|
+
## [0.9.0] — 2025-03-10
|
2
|
+
|
3
|
+
### Changed
|
4
|
+
|
5
|
+
- Requires Rails 8 and Ruby 3.4+.
|
6
|
+
- Protected `AdjustableSchema::Relationship::Role` from being deleted when used.
|
7
|
+
|
8
|
+
### Added
|
9
|
+
|
10
|
+
- `Relationship#sourced` and `Relationship#targeted` scopes to filter relationships by presence of _source_ and _target_ records respectively.
|
11
|
+
|
12
|
+
### Fixed
|
13
|
+
|
14
|
+
- `Relationship#applied` scope used to return relationships with at least one record attached, instead of ones with both records present.
|
15
|
+
|
16
|
+
|
1
17
|
## [0.8.0] — 2024-11-08
|
2
18
|
|
3
19
|
### Changed
|
data/Rakefile
CHANGED
@@ -1,8 +1,10 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
load "rails/tasks/engine.rake"
|
3
|
+
require 'bundler/setup'
|
5
4
|
|
6
|
-
|
5
|
+
APP_RAKEFILE = File.expand_path('test/dummy/Rakefile', __dir__)
|
6
|
+
load 'rails/tasks/engine.rake'
|
7
7
|
|
8
|
-
|
8
|
+
load 'rails/tasks/statistics.rake'
|
9
|
+
|
10
|
+
require 'bundler/gem_tasks'
|
@@ -1,9 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module AdjustableSchema
|
2
4
|
class Relationship
|
5
|
+
# = Relationship roles
|
6
|
+
#
|
7
|
+
# `AdjustableSchema::Relationship::Role` serves to distinguish
|
8
|
+
# between several associations of the same pair of models.
|
3
9
|
class Role < ApplicationRecord
|
4
10
|
include Organizer::Identifiable.by :name, symbolized: true
|
5
11
|
|
6
|
-
has_many :relationships
|
12
|
+
has_many :relationships,
|
13
|
+
dependent: :restrict_with_exception
|
7
14
|
|
8
15
|
validates :name, presence: true, uniqueness: true
|
9
16
|
|
@@ -1,4 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module AdjustableSchema
|
4
|
+
# = Relationships
|
5
|
+
#
|
6
|
+
# `Relationship` is a core class of Adjustable Schema representing both
|
7
|
+
# associations of model classes and connections of individual records.
|
8
|
+
#
|
9
|
+
# No constraints are supported yet, so only many-to-many associations
|
10
|
+
# are available for now.
|
11
|
+
#
|
12
|
+
# == Model associations
|
13
|
+
#
|
14
|
+
# To represent an association, a `Relationship` record should have both
|
15
|
+
# `source_type` and `target_type` columns set, with both ID columns set
|
16
|
+
# to `NULL`.
|
17
|
+
#
|
18
|
+
# == Record relationships
|
19
|
+
#
|
20
|
+
# To connect individual records, both `source` and `target` polymorphic
|
21
|
+
# associations shoud be set.
|
22
|
+
#
|
23
|
+
# == Roles
|
24
|
+
#
|
25
|
+
# Many associations with different semantics between the same models
|
26
|
+
# can be set using roles (see `Relationship::Role`).
|
2
27
|
class Relationship < ApplicationRecord
|
3
28
|
belongs_to :source, polymorphic: true, optional: true
|
4
29
|
belongs_to :target, polymorphic: true, optional: true
|
@@ -8,7 +33,7 @@ module AdjustableSchema
|
|
8
33
|
includes :role
|
9
34
|
end
|
10
35
|
|
11
|
-
Config.shortcuts.each &-> ((association, method)) do
|
36
|
+
Config.shortcuts.each &-> ((association, method)) do # rubocop:disable Style
|
12
37
|
scope method, -> object {
|
13
38
|
case object
|
14
39
|
when ::ActiveRecord::Base, nil
|
@@ -29,8 +54,7 @@ module AdjustableSchema
|
|
29
54
|
|
30
55
|
scope "#{method}_abstract", -> object = nil {
|
31
56
|
if object
|
32
|
-
send(__method__).
|
33
|
-
send method, object
|
57
|
+
send(__method__).send method, object
|
34
58
|
else
|
35
59
|
where "#{association}_id" => nil
|
36
60
|
end
|
@@ -39,20 +63,14 @@ module AdjustableSchema
|
|
39
63
|
|
40
64
|
scope :abstract, -> {
|
41
65
|
Config.shortcuts.values
|
42
|
-
.map { send
|
66
|
+
.map { send it, :abstract }
|
43
67
|
.reduce &:merge
|
44
68
|
}
|
45
69
|
|
46
|
-
scope :general,
|
47
|
-
|
48
|
-
}
|
49
|
-
|
50
|
-
scope :applied, -> {
|
51
|
-
where.not(
|
52
|
-
source: nil,
|
53
|
-
target: nil,
|
54
|
-
)
|
55
|
-
}
|
70
|
+
scope :general, -> { where target: nil }
|
71
|
+
scope :sourced, -> { where.not source: nil }
|
72
|
+
scope :targeted, -> { where.not target: nil }
|
73
|
+
scope :applied, -> { sourced.targeted }
|
56
74
|
|
57
75
|
scope :named, -> *names {
|
58
76
|
case names
|
@@ -74,15 +92,15 @@ module AdjustableSchema
|
|
74
92
|
def [] **scopes
|
75
93
|
scopes
|
76
94
|
.map do
|
77
|
-
self
|
95
|
+
self # rubocop:disable Style
|
78
96
|
.send(Config.shortcuts[:source], _1)
|
79
97
|
.send(Config.shortcuts[:target], _2)
|
80
98
|
end
|
81
99
|
.reduce &:or
|
82
100
|
end
|
83
101
|
|
84
|
-
def seed! *models, roles: [], **
|
85
|
-
return seed!({ **Hash[*models], **
|
102
|
+
def seed! *models, roles: [], **mapping # rubocop:disable Metrics
|
103
|
+
return seed!({ **Hash[*models], **mapping }, roles:) if mapping.any? # support keyword arguments syntax
|
86
104
|
|
87
105
|
case models
|
88
106
|
in [
|
@@ -91,7 +109,7 @@ module AdjustableSchema
|
|
91
109
|
]
|
92
110
|
roles
|
93
111
|
.map { |name| Role.find_or_create_by! name: }
|
94
|
-
.then {
|
112
|
+
.then { it.presence or [ nil ] } # no roles => nameless relationship
|
95
113
|
.map { |role| create! source_type:, target_type:, role: }
|
96
114
|
in [ Hash => models ]
|
97
115
|
for sources, targets in models do
|
@@ -122,7 +140,7 @@ module AdjustableSchema
|
|
122
140
|
# https://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#module-ActiveRecord::Associations::ClassMethods-label-Polymorphic+Associations
|
123
141
|
reflections
|
124
142
|
.values
|
125
|
-
.select {
|
143
|
+
.select { it.options[:polymorphic] }
|
126
144
|
.each do |reflection|
|
127
145
|
define_method "#{reflection.name}_type=" do |type|
|
128
146
|
super type && type.to_s.classify.constantize.base_class.to_s
|
@@ -1,14 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'memery'
|
2
4
|
|
3
5
|
module AdjustableSchema
|
4
|
-
module ActiveRecord
|
6
|
+
module ActiveRecord # :nodoc:
|
5
7
|
concern :Relationships do
|
6
8
|
class_methods do
|
7
9
|
include Memery
|
8
10
|
|
9
11
|
memoize def relationships
|
10
|
-
Config.association_directions.
|
11
|
-
|
12
|
+
Config.association_directions.index_with do
|
13
|
+
Relationship.abstract.send Config.shortcuts.opposite[it], self
|
12
14
|
end
|
13
15
|
end
|
14
16
|
|
@@ -44,20 +46,20 @@ module AdjustableSchema
|
|
44
46
|
.preload(Config.association_directions)
|
45
47
|
.map do |relationship|
|
46
48
|
Config.association_directions
|
47
|
-
.map { relationship.send
|
49
|
+
.map { relationship.send it } # both objects
|
48
50
|
.without(self) # the related one
|
49
51
|
.first or self # may be recursive
|
50
52
|
end
|
51
53
|
.uniq
|
52
54
|
end
|
53
55
|
|
54
|
-
def relationships
|
55
|
-
if (direction, scope = Config.find_direction
|
56
|
+
def relationships(...)
|
57
|
+
if (direction, scope = Config.find_direction(...)) # filter by direction & related objects
|
56
58
|
relationships_to(direction)
|
57
59
|
.send Config.shortcuts[direction], scope
|
58
60
|
else # all in both directions
|
59
61
|
Config.association_directions
|
60
|
-
.map { relationships_to
|
62
|
+
.map { relationships_to it }
|
61
63
|
.reduce(&:or)
|
62
64
|
end
|
63
65
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
class CreateAdjustableSchemaRelationshipTables < ActiveRecord::Migration[7.1]
|
2
4
|
def change
|
3
5
|
# Use Active Record's configured type for primary and foreign keys
|
@@ -22,7 +24,7 @@ class CreateAdjustableSchemaRelationshipTables < ActiveRecord::Migration[7.1]
|
|
22
24
|
target_id target_type
|
23
25
|
role_id
|
24
26
|
].tap { |columns|
|
25
|
-
columns.reject! {
|
27
|
+
columns.reject! { it.ends_with? '_type' } if foreign_key_type == :uuid # OPTIMIZATION: IDs are unique
|
26
28
|
|
27
29
|
# NULLS are DISTINCT by default.
|
28
30
|
# One can use `ADD CONSTRAINT … UNIQUE NULLS NOT DISTINCT (…)` instead
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AdjustableSchema
|
4
|
+
module ActiveRecord
|
5
|
+
module Association::Hierarchy # :nodoc:
|
6
|
+
private
|
7
|
+
|
8
|
+
def scopes
|
9
|
+
name = roleless_name # save the context
|
10
|
+
|
11
|
+
{
|
12
|
+
**super,
|
13
|
+
|
14
|
+
name_for_any( target_name) => -> { where.associated name },
|
15
|
+
name_for_none(target_name) => -> { where.missing name },
|
16
|
+
}
|
17
|
+
end
|
18
|
+
|
19
|
+
def flags
|
20
|
+
name = roleless_name # save the context
|
21
|
+
|
22
|
+
{
|
23
|
+
**super,
|
24
|
+
|
25
|
+
name_for_any( target_name) => -> { send(name).any? },
|
26
|
+
name_for_none(target_name) => -> { send(name).none? },
|
27
|
+
intermediate: -> { send(name).one? },
|
28
|
+
branching: -> { send(name).many? },
|
29
|
+
}
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -1,16 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'memery'
|
2
4
|
|
3
5
|
module AdjustableSchema
|
4
6
|
module ActiveRecord
|
5
|
-
class Association
|
7
|
+
class Association # :nodoc:
|
6
8
|
concerning :Naming do
|
7
9
|
include Memery
|
8
10
|
|
9
|
-
module Inflections
|
11
|
+
module Inflections # :nodoc:
|
10
12
|
refine String do
|
11
13
|
def passivize
|
12
|
-
|
13
|
-
.presence
|
14
|
+
presence
|
14
15
|
&.sub(/((:?[aeiou]+[^aeiou]+){2,})(?:or|ant|ion|e?ment)$/, '\1ed')
|
15
16
|
&.sub(/((:?[aeiou]+[^aeiou]+){1,})(?:ing)$/, '\1ed')
|
16
17
|
&.sub(/(?:e*|ed|er)$/, '\1ed')
|
@@ -21,6 +22,27 @@ module AdjustableSchema
|
|
21
22
|
|
22
23
|
using Inflections
|
23
24
|
|
25
|
+
module Recursive # :nodoc:
|
26
|
+
include Memery
|
27
|
+
|
28
|
+
memoize def name_with_role = {
|
29
|
+
source: role.name,
|
30
|
+
target: "#{role.name.passivize}_#{target_name}",
|
31
|
+
}[direction]
|
32
|
+
|
33
|
+
def name_without_role
|
34
|
+
Config.association_directions
|
35
|
+
.self[direction]
|
36
|
+
.to_s
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def initialize(...)
|
41
|
+
super
|
42
|
+
|
43
|
+
extend Recursive if recursive?
|
44
|
+
end
|
45
|
+
|
24
46
|
memoize def name name = object_name
|
25
47
|
name
|
26
48
|
.to_s
|
@@ -48,29 +70,12 @@ module AdjustableSchema
|
|
48
70
|
|
49
71
|
private
|
50
72
|
|
51
|
-
memoize def name_with_role
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
target: "#{role.name.passivize}_#{target_name}",
|
56
|
-
}[direction]
|
57
|
-
else
|
58
|
-
"#{{
|
59
|
-
source: role.name,
|
60
|
-
target: role.name.passivize,
|
61
|
-
}[direction]}_#{target_name}"
|
62
|
-
end
|
63
|
-
end
|
73
|
+
memoize def name_with_role = "#{{
|
74
|
+
source: role.name,
|
75
|
+
target: role.name.passivize,
|
76
|
+
}[direction]}_#{target_name}"
|
64
77
|
|
65
|
-
def name_without_role
|
66
|
-
if recursive?
|
67
|
-
Config.association_directions
|
68
|
-
.self[direction]
|
69
|
-
.to_s
|
70
|
-
else
|
71
|
-
target_name
|
72
|
-
end
|
73
|
-
end
|
78
|
+
def name_without_role = target_name
|
74
79
|
|
75
80
|
def roleless_name = name(target_name)
|
76
81
|
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AdjustableSchema
|
4
|
+
module ActiveRecord
|
5
|
+
module Association::Roleless # :nodoc:
|
6
|
+
def define
|
7
|
+
super
|
8
|
+
|
9
|
+
has_many roleless_name, -> { roleless }, **options if
|
10
|
+
child?
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def define_methods
|
16
|
+
super
|
17
|
+
|
18
|
+
name = self.name # save the context
|
19
|
+
|
20
|
+
owner.redefine_method "#{name}_with_roles" do |*roles|
|
21
|
+
send(name)
|
22
|
+
.merge Relationship.named *roles
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -1,39 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module AdjustableSchema
|
2
4
|
module ActiveRecord
|
3
5
|
module Association::Scopes
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
included do
|
8
|
-
::ActiveRecord::QueryMethods.prepend QueryMethods # HACK: to bring `with.recursive` in
|
9
|
-
end
|
6
|
+
module Recursive # :nodoc:
|
7
|
+
# rubocop:disable Layout
|
10
8
|
|
11
9
|
def recursive
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
)
|
18
|
-
.with.recursive(recursive_table.name => unscoped
|
19
|
-
.select(
|
20
|
-
select_values,
|
21
|
-
(recursive_table[:distance] + 1).as('distance'),
|
22
|
-
)
|
23
|
-
.joins(inverse_association_name)
|
24
|
-
.arel
|
25
|
-
.join(recursive_table)
|
26
|
-
.on(recursive_table[primary_key].eq inverse_table[primary_key])
|
27
|
-
)
|
28
|
-
.unscope(:select, :joins, :where)
|
29
|
-
.from(recursive_table.alias table_name)
|
30
|
-
.distinct
|
31
|
-
.unscope(:order) # for SELECT DISTINCT, ORDER BY expressions must appear in select list
|
32
|
-
end
|
10
|
+
with_recursive(recursive_table.name => [ recursion_base, recursive_step ])
|
11
|
+
.unscope(:select, :joins, :where)
|
12
|
+
.from(recursive_table.alias table_name)
|
13
|
+
.distinct
|
14
|
+
.unscope(:order) # for SELECT DISTINCT, ORDER BY expressions must appear in select list
|
33
15
|
end
|
34
16
|
|
35
17
|
private
|
36
18
|
|
19
|
+
def recursion_base
|
20
|
+
unscope(:order, :group, :having)
|
21
|
+
.select(recursive_select_values,
|
22
|
+
Arel.sql('1').as('distance'),
|
23
|
+
)
|
24
|
+
end
|
25
|
+
|
26
|
+
def recursive_step
|
27
|
+
unscoped
|
28
|
+
.select(recursive_select_values,
|
29
|
+
(recursive_table[:distance] + 1).as('distance'),
|
30
|
+
)
|
31
|
+
.joins(inverse_association_name)
|
32
|
+
.joins(<<~SQL.squish)
|
33
|
+
JOIN #{recursive_table.name}
|
34
|
+
ON #{recursive_table.name}.#{primary_key} = #{inverse_table.name}.#{primary_key}
|
35
|
+
SQL
|
36
|
+
end
|
37
|
+
|
37
38
|
def association_name = @association.reflection.name
|
38
39
|
|
39
40
|
def inverse_association_name
|
@@ -46,6 +47,8 @@ module AdjustableSchema
|
|
46
47
|
|
47
48
|
def recursive_table = Arel::Table.new [ :recursive, association_name, klass.table_name ] * '_'
|
48
49
|
def inverse_table = Arel::Table.new [ inverse_association_name, klass.table_name ] * '_' # HACK: depends on ActiveRecord internals
|
50
|
+
|
51
|
+
def recursive_select_values = select_values.presence || arel_table[Arel.star]
|
49
52
|
end
|
50
53
|
end
|
51
54
|
end
|
@@ -1,35 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'memery'
|
4
|
+
|
1
5
|
module AdjustableSchema
|
2
6
|
module ActiveRecord
|
3
|
-
|
7
|
+
# rubocop:disable Style/StructInheritance
|
8
|
+
class Association < Struct.new( # :nodoc:
|
9
|
+
:owner,
|
10
|
+
:direction,
|
11
|
+
:target,
|
12
|
+
:role,
|
13
|
+
)
|
4
14
|
require_relative 'association/naming'
|
5
15
|
require_relative 'association/scopes'
|
16
|
+
require_relative 'association/roleless'
|
17
|
+
require_relative 'association/hierarchy'
|
18
|
+
|
19
|
+
include Memery
|
20
|
+
|
21
|
+
def initialize(...)
|
22
|
+
super
|
23
|
+
|
24
|
+
extend Roleless if roleless?
|
25
|
+
extend Hierarchy if hierarchy?
|
26
|
+
end
|
6
27
|
|
7
28
|
def define
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
source: direction,
|
14
|
-
source_type: target.base_class.name,
|
15
|
-
class_name: target.name
|
16
|
-
}) do
|
17
|
-
include Scopes
|
18
|
-
include Scopes::Recursive if association.recursive?
|
19
|
-
end
|
20
|
-
|
21
|
-
define_scopes
|
22
|
-
define_methods
|
23
|
-
|
24
|
-
unless role
|
25
|
-
# HACK: using `try` to overcome a Rails bug
|
26
|
-
# (see https://github.com/rails/rails/issues/40109)
|
27
|
-
has_many roleless_name, -> { try :roleless }, **options if
|
28
|
-
child?
|
29
|
-
|
30
|
-
define_role_methods
|
31
|
-
end
|
29
|
+
association = self # save the context
|
30
|
+
|
31
|
+
has_many name, **options do
|
32
|
+
include Scopes
|
33
|
+
include Scopes::Recursive if association.recursive?
|
32
34
|
end
|
35
|
+
|
36
|
+
define_scopes
|
37
|
+
define_methods
|
33
38
|
end
|
34
39
|
|
35
40
|
def recursive? = target == owner
|
@@ -52,56 +57,48 @@ module AdjustableSchema
|
|
52
57
|
end
|
53
58
|
|
54
59
|
def define_scopes
|
55
|
-
|
56
|
-
roleless_name = self.roleless_name
|
57
|
-
|
58
|
-
{
|
59
|
-
name_for_any => -> { where.associated name },
|
60
|
-
name_for_none => -> { where.missing name },
|
61
|
-
|
62
|
-
**({
|
63
|
-
name_for_any( target_name) => -> { where.associated roleless_name },
|
64
|
-
name_for_none(target_name) => -> { where.missing roleless_name },
|
65
|
-
} if hierarchy?),
|
66
|
-
}
|
60
|
+
scopes
|
67
61
|
.reject { owner.singleton_class.method_defined? _1 }
|
68
62
|
.each { owner.scope _1, _2 }
|
69
63
|
end
|
70
64
|
|
71
65
|
def define_methods
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
{
|
76
|
-
name_for_any => -> { send(name).any? },
|
77
|
-
name_for_none => -> { send(name).none? },
|
78
|
-
|
79
|
-
**(hierarchy? ? {
|
80
|
-
name_for_any( target_name) => -> { send(roleless_name).any? },
|
81
|
-
name_for_none(target_name) => -> { send(roleless_name).none? },
|
82
|
-
intermediate: -> { send(roleless_name).one? },
|
83
|
-
branching: -> { send(roleless_name).many? },
|
84
|
-
} : {}),
|
85
|
-
}
|
86
|
-
.transform_keys {"#{_1}?" }
|
66
|
+
flags
|
67
|
+
.transform_keys { :"#{it}?" }
|
87
68
|
.reject { owner.method_defined? _1 }
|
88
69
|
.each { owner.define_method _1, &_2 }
|
89
70
|
end
|
90
71
|
|
91
|
-
def define_role_methods
|
92
|
-
name = self.name
|
93
|
-
|
94
|
-
owner.redefine_method "#{name}_with_roles" do |*roles|
|
95
|
-
send(name)
|
96
|
-
.merge Relationship.named *roles
|
97
|
-
end
|
98
|
-
end
|
99
|
-
|
100
72
|
def has_many(association_name, ...)
|
101
73
|
return if owner.reflect_on_association association_name
|
102
74
|
|
103
75
|
owner.has_many(association_name, ...)
|
104
76
|
end
|
77
|
+
|
78
|
+
def scopes
|
79
|
+
name = relationships_name # save the context
|
80
|
+
|
81
|
+
{
|
82
|
+
name_for_any => -> { where.associated name },
|
83
|
+
name_for_none => -> { where.missing name },
|
84
|
+
}
|
85
|
+
end
|
86
|
+
|
87
|
+
def flags
|
88
|
+
name = self.name # save the context
|
89
|
+
|
90
|
+
{
|
91
|
+
name_for_any => -> { send(name).any? },
|
92
|
+
name_for_none => -> { send(name).none? },
|
93
|
+
}
|
94
|
+
end
|
95
|
+
|
96
|
+
memoize def options = {
|
97
|
+
through: define_relationships,
|
98
|
+
source: direction,
|
99
|
+
source_type: target.base_class.name,
|
100
|
+
class_name: target.name,
|
101
|
+
}
|
105
102
|
end
|
106
103
|
end
|
107
104
|
end
|
@@ -1,10 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'memery'
|
2
4
|
|
3
5
|
module AdjustableSchema
|
4
|
-
module Config
|
6
|
+
module Config # :nodoc:
|
5
7
|
include Memery
|
6
8
|
|
7
|
-
module Naming
|
9
|
+
module Naming # :nodoc:
|
8
10
|
include Memery
|
9
11
|
|
10
12
|
memoize def shortcuts
|
@@ -13,7 +15,7 @@ module AdjustableSchema
|
|
13
15
|
if to
|
14
16
|
values.grep_v(to).sole
|
15
17
|
else
|
16
|
-
transform_values { opposite to:
|
18
|
+
transform_values { opposite to: it }
|
17
19
|
end
|
18
20
|
end
|
19
21
|
end
|
@@ -23,7 +25,7 @@ module AdjustableSchema
|
|
23
25
|
|
24
26
|
def recursive
|
25
27
|
config.values.to_h do
|
26
|
-
[
|
28
|
+
[ it[:self].to_s.pluralize.to_sym, it[:recursive].to_sym ]
|
27
29
|
end
|
28
30
|
end
|
29
31
|
|
@@ -35,7 +37,7 @@ module AdjustableSchema
|
|
35
37
|
|
36
38
|
def config section = nil
|
37
39
|
if section
|
38
|
-
config.transform_values {
|
40
|
+
config.transform_values { it[section].to_sym }
|
39
41
|
else
|
40
42
|
Config.association_names # TODO: DRY
|
41
43
|
end
|
@@ -64,14 +66,11 @@ module AdjustableSchema
|
|
64
66
|
module_function method
|
65
67
|
end
|
66
68
|
|
67
|
-
private
|
68
|
-
module_function
|
69
|
-
|
70
69
|
def association_names = Engine.config.names[:associations]
|
71
70
|
|
72
71
|
def normalize **options
|
73
72
|
shortcuts
|
74
|
-
.tap { options.assert_valid_keys
|
73
|
+
.tap { options.assert_valid_keys it.keys, it.values }
|
75
74
|
.select { _2.in? options }
|
76
75
|
.each { options[_1] = options.delete _2 }
|
77
76
|
|
data/lib/adjustable_schema.rb
CHANGED
@@ -1,10 +1,12 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'adjustable_schema/version'
|
4
|
+
require 'adjustable_schema/engine'
|
3
5
|
require 'adjustable_schema/active_record'
|
4
6
|
|
5
7
|
require 'rails_model_load_hook' # should be loaded
|
6
8
|
|
7
|
-
module AdjustableSchema
|
9
|
+
module AdjustableSchema # :nodoc:
|
8
10
|
autoload :Config, 'adjustable_schema/config'
|
9
11
|
|
10
12
|
module_function
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: adjustable_schema
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.9.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Alexander Senko
|
8
8
|
bindir: bin
|
9
9
|
cert_chain: []
|
10
|
-
date:
|
10
|
+
date: 2025-03-09 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: rails
|
@@ -15,14 +15,14 @@ dependencies:
|
|
15
15
|
requirements:
|
16
16
|
- - "~>"
|
17
17
|
- !ruby/object:Gem::Version
|
18
|
-
version: '
|
18
|
+
version: '8.0'
|
19
19
|
type: :runtime
|
20
20
|
prerelease: false
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
22
22
|
requirements:
|
23
23
|
- - "~>"
|
24
24
|
- !ruby/object:Gem::Version
|
25
|
-
version: '
|
25
|
+
version: '8.0'
|
26
26
|
- !ruby/object:Gem::Dependency
|
27
27
|
name: rails_model_load_hook
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
@@ -77,28 +77,21 @@ files:
|
|
77
77
|
- MIT-LICENSE
|
78
78
|
- README.md
|
79
79
|
- Rakefile
|
80
|
-
- app/assets/config/adjustable_schema_manifest.js
|
81
|
-
- app/assets/stylesheets/adjustable_schema/application.css
|
82
|
-
- app/controllers/adjustable_schema/application_controller.rb
|
83
|
-
- app/helpers/adjustable_schema/application_helper.rb
|
84
|
-
- app/jobs/adjustable_schema/application_job.rb
|
85
|
-
- app/mailers/adjustable_schema/application_mailer.rb
|
86
80
|
- app/models/adjustable_schema/application_record.rb
|
87
81
|
- app/models/adjustable_schema/relationship.rb
|
88
82
|
- app/models/adjustable_schema/relationship/role.rb
|
89
83
|
- app/models/concerns/adjustable_schema/active_record/associations.rb
|
90
84
|
- app/models/concerns/adjustable_schema/active_record/relationships.rb
|
91
|
-
- app/views/layouts/adjustable_schema/application.html.erb
|
92
85
|
- config/initializers/associations.rb
|
93
86
|
- config/initializers/model_names.rb
|
94
|
-
- config/routes.rb
|
95
87
|
- db/migrate/01_create_adjustable_schema_relationship_tables.rb
|
96
88
|
- lib/adjustable_schema.rb
|
97
89
|
- lib/adjustable_schema/active_record.rb
|
98
90
|
- lib/adjustable_schema/active_record/association.rb
|
91
|
+
- lib/adjustable_schema/active_record/association/hierarchy.rb
|
99
92
|
- lib/adjustable_schema/active_record/association/naming.rb
|
93
|
+
- lib/adjustable_schema/active_record/association/roleless.rb
|
100
94
|
- lib/adjustable_schema/active_record/association/scopes.rb
|
101
|
-
- lib/adjustable_schema/active_record/query_methods.rb
|
102
95
|
- lib/adjustable_schema/authors.rb
|
103
96
|
- lib/adjustable_schema/config.rb
|
104
97
|
- lib/adjustable_schema/engine.rb
|
@@ -110,7 +103,7 @@ licenses:
|
|
110
103
|
metadata:
|
111
104
|
homepage_uri: https://github.com/Alexander-Senko/adjustable_schema
|
112
105
|
source_code_uri: https://github.com/Alexander-Senko/adjustable_schema
|
113
|
-
changelog_uri: https://github.com/Alexander-Senko/adjustable_schema/blob/v0.
|
106
|
+
changelog_uri: https://github.com/Alexander-Senko/adjustable_schema/blob/v0.9.0/CHANGELOG.md
|
114
107
|
rdoc_options: []
|
115
108
|
require_paths:
|
116
109
|
- lib
|
@@ -118,14 +111,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
118
111
|
requirements:
|
119
112
|
- - ">="
|
120
113
|
- !ruby/object:Gem::Version
|
121
|
-
version: '3.
|
114
|
+
version: '3.4'
|
122
115
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
123
116
|
requirements:
|
124
117
|
- - ">="
|
125
118
|
- !ruby/object:Gem::Version
|
126
119
|
version: '0'
|
127
120
|
requirements: []
|
128
|
-
rubygems_version: 3.6.
|
121
|
+
rubygems_version: 3.6.5
|
129
122
|
specification_version: 4
|
130
123
|
summary: Adjustable data schemas for Rails
|
131
124
|
test_files: []
|
@@ -1 +0,0 @@
|
|
1
|
-
//= link_directory ../stylesheets/adjustable_schema .css
|
@@ -1,15 +0,0 @@
|
|
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
|
-
*/
|
@@ -1,15 +0,0 @@
|
|
1
|
-
<!DOCTYPE html>
|
2
|
-
<html>
|
3
|
-
<head>
|
4
|
-
<title>Adjustable schema</title>
|
5
|
-
<%= csrf_meta_tags %>
|
6
|
-
<%= csp_meta_tag %>
|
7
|
-
|
8
|
-
<%= stylesheet_link_tag "adjustable_schema/application", media: "all" %>
|
9
|
-
</head>
|
10
|
-
<body>
|
11
|
-
|
12
|
-
<%= yield %>
|
13
|
-
|
14
|
-
</body>
|
15
|
-
</html>
|
data/config/routes.rb
DELETED
@@ -1,103 +0,0 @@
|
|
1
|
-
module AdjustableSchema
|
2
|
-
module ActiveRecord
|
3
|
-
module QueryMethods
|
4
|
-
class WithChain
|
5
|
-
def initialize scope
|
6
|
-
@scope = scope
|
7
|
-
end
|
8
|
-
|
9
|
-
# Returns a new relation expressing WITH RECURSIVE statement.
|
10
|
-
#
|
11
|
-
# #recursive accepts conditions as a hash. See QueryMethods#with for
|
12
|
-
# more details on each format.
|
13
|
-
#
|
14
|
-
# User.with.recursive(
|
15
|
-
# descendants: User.joins('INNER JOIN descendants ON users.parent_id = descendants.id')
|
16
|
-
# )
|
17
|
-
# # WITH RECURSIVE descendants AS (
|
18
|
-
# # SELECT * FROM users
|
19
|
-
# # UNION
|
20
|
-
# # SELECT * FROM users INNER JOIN descendants ON users.parent_id = descendants.id
|
21
|
-
# # ) SELECT * FROM descendants
|
22
|
-
#
|
23
|
-
# WARNING! Due to how Arel works,
|
24
|
-
# * `recursive` can't be chained with any prior non-recursive calls to `with` and
|
25
|
-
# * all subsequent non-recursive calls to `with` will be treated as recursive ones.
|
26
|
-
def recursive *args
|
27
|
-
args.map! do
|
28
|
-
next _1 unless _1.is_a? Hash
|
29
|
-
|
30
|
-
_1
|
31
|
-
.map(&method(:with_recursive_union))
|
32
|
-
.to_h
|
33
|
-
end
|
34
|
-
|
35
|
-
case @scope.with_values
|
36
|
-
in []
|
37
|
-
@scope = @scope.with :recursive, *args
|
38
|
-
in [ :recursive, * ]
|
39
|
-
@scope = @scope.with *args
|
40
|
-
else
|
41
|
-
raise ArgumentError, "can't chain `WITH RECURSIVE` with non-recursive one"
|
42
|
-
end
|
43
|
-
|
44
|
-
@scope
|
45
|
-
end
|
46
|
-
|
47
|
-
private
|
48
|
-
|
49
|
-
def with_recursive_union name, scope
|
50
|
-
scope = scope.arel if scope.respond_to? :arel
|
51
|
-
|
52
|
-
[
|
53
|
-
name,
|
54
|
-
@scope
|
55
|
-
.unscope(:order, :group, :having)
|
56
|
-
.arel
|
57
|
-
.union(scope)
|
58
|
-
]
|
59
|
-
end
|
60
|
-
end
|
61
|
-
|
62
|
-
def with *args
|
63
|
-
if args.empty?
|
64
|
-
WithChain.new spawn
|
65
|
-
else
|
66
|
-
super
|
67
|
-
end
|
68
|
-
end
|
69
|
-
|
70
|
-
private
|
71
|
-
|
72
|
-
# OVERWRITE: allow a Symbol to be passes as the first argument
|
73
|
-
def build_with(arel)
|
74
|
-
return if with_values.empty?
|
75
|
-
|
76
|
-
with_statements = with_values.map.with_index do |with_value, i|
|
77
|
-
next with_value if with_value.is_a? Symbol and i == 0
|
78
|
-
|
79
|
-
raise ArgumentError, "Unsupported argument type: #{with_value} #{with_value.class}" unless with_value.is_a?(Hash)
|
80
|
-
|
81
|
-
build_with_value_from_hash(with_value)
|
82
|
-
end
|
83
|
-
|
84
|
-
arel.with(*with_statements)
|
85
|
-
end
|
86
|
-
|
87
|
-
# OVERWRITE: allow Arel Nodes
|
88
|
-
def build_with_value_from_hash(hash)
|
89
|
-
hash.map do |name, value|
|
90
|
-
expression =
|
91
|
-
case value
|
92
|
-
when Arel::Nodes::SqlLiteral then Arel::Nodes::Grouping.new(value)
|
93
|
-
when ::ActiveRecord::Relation then value.arel
|
94
|
-
when Arel::SelectManager, Arel::Nodes::Node then value
|
95
|
-
else
|
96
|
-
raise ArgumentError, "Unsupported argument type: `#{value}` #{value.class}"
|
97
|
-
end
|
98
|
-
Arel::Nodes::TableAlias.new(expression, name)
|
99
|
-
end
|
100
|
-
end
|
101
|
-
end
|
102
|
-
end
|
103
|
-
end
|