rails-fields 0.3.1 → 0.3.3
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/README.md +5 -3
- data/lib/rails/fields.rb +3 -0
- data/lib/rails_fields/class_methods.rb +12 -4
- data/lib/rails_fields/railtie.rb +14 -2
- data/lib/rails_fields/utils/helpers.rb +31 -18
- data/lib/rails_fields/utils/mappings.rb +27 -24
- data/lib/rails_fields/version.rb +1 -1
- metadata +7 -23
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 72386aef5bd4ef9136597eb8226759ce716deb2d4a899b27837031c3d4678596
|
4
|
+
data.tar.gz: b0a8d07705dfba50be8270da36854db49cedc9d2c10f09b292cc48674f059965
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f5b4d8dd33a0f8b4103c649901e7f7c789aea02a64411998db0f5a722c811a59b1107a1e86b4c73f9d201173376789c76b6b5664ef46a96ecacca08994efcf22
|
7
|
+
data.tar.gz: 59ed93e40c507e75a139f2321c011101070e200d5d217e4eb1752cf085e60983b614f60511fad534b64c8eb4e8dca58d6d2930c24d3e2b844d928d366c8570fc
|
data/README.md
CHANGED
@@ -2,13 +2,13 @@
|
|
2
2
|
<a href="https://rails-fields.dev" target="_blank"><img src="./assets/logo.svg" width="300" /></a>
|
3
3
|
</p>
|
4
4
|
|
5
|
-
[](https://badge.fury.io/rb/rails-fields)
|
5
|
+
[](https://badge.fury.io/rb/rails-fields)
|
6
6
|
|
7
7
|
# Rails Fields
|
8
8
|
|
9
9
|
Enforce field types and attributes for ActiveRecord models in Ruby on Rails applications.
|
10
10
|
|
11
|
-
- 🚀
|
11
|
+
- 🚀 Automatic ActiveRecord **Migrations** generation
|
12
12
|
- 🦄 Automatic [GraphQL types](https://graphql-ruby.org/type_definitions/objects.html) generation
|
13
13
|
- 📝 Explicit **declarative** model attributes annotation
|
14
14
|
- 💪🏻 Enforcement of fields declaration with real db columns
|
@@ -83,4 +83,6 @@ The gem is available as open source under the terms of the [MIT License](https:/
|
|
83
83
|
|
84
84
|
## Author
|
85
85
|
|
86
|
-
Gaston Morixe - gaston@gastonmorixe.com
|
86
|
+
Gaston Morixe 2023 - gaston@gastonmorixe.com
|
87
|
+
|
88
|
+
[rails-fields.dev](https://rails-fields.dev/?gh)
|
data/lib/rails/fields.rb
ADDED
@@ -1,4 +1,7 @@
|
|
1
1
|
module RailsFields
|
2
|
+
# Lightweight, typed container for field declarations
|
3
|
+
# Include :options to maintain compatibility with code that reads it
|
4
|
+
DeclaredField = Struct.new(:name, :type, :null, :index, :options, keyword_init: true)
|
2
5
|
module ClassMethods
|
3
6
|
# TODO: Check all models at rails init app? like migrations?
|
4
7
|
|
@@ -36,16 +39,20 @@ module RailsFields
|
|
36
39
|
")
|
37
40
|
end
|
38
41
|
|
39
|
-
declared_fields <<
|
42
|
+
declared_fields << DeclaredField.new(name: name.to_s, type:, null:, index:)
|
40
43
|
end
|
41
44
|
|
42
45
|
def gql_type
|
46
|
+
unless defined?(GraphQL)
|
47
|
+
raise "GraphQL is not available. Add `gem 'graphql'` and install it, or avoid calling `gql_type`."
|
48
|
+
end
|
43
49
|
return RailsFields.processed_classes[self] if RailsFields.processed_classes[self].present?
|
44
50
|
|
45
51
|
fields = declared_fields
|
46
52
|
owner_self = self
|
47
53
|
|
48
|
-
|
54
|
+
base_object = defined?(::Types::BaseObject) ? ::Types::BaseObject : ::GraphQL::Schema::Object
|
55
|
+
type = Class.new(base_object) do
|
49
56
|
# graphql_name "#{owner_self.name}Type"
|
50
57
|
graphql_name "#{owner_self.name}"
|
51
58
|
description "A type representing a #{owner_self.name}"
|
@@ -55,8 +62,9 @@ module RailsFields
|
|
55
62
|
|
56
63
|
# Assuming a proper mapping from your custom types to GraphQL types
|
57
64
|
# TODO: use a better method or block
|
58
|
-
|
59
|
-
|
65
|
+
field_name = f.name.to_s
|
66
|
+
field_gql_type = field_name == 'id' ? GraphQL::Types::ID : Utils::RAILS_TO_GQL_TYPE_MAP[f.type]
|
67
|
+
field field_name, field_gql_type
|
60
68
|
end
|
61
69
|
end
|
62
70
|
|
data/lib/rails_fields/railtie.rb
CHANGED
@@ -14,7 +14,19 @@ module RailsFields
|
|
14
14
|
end
|
15
15
|
|
16
16
|
initializer "rails_fields.middleware" do |app|
|
17
|
-
|
17
|
+
# In Rails 8, ActiveRecord::Migration::CheckPending was removed.
|
18
|
+
# Try to insert after it when present; otherwise, append the middleware.
|
19
|
+
if defined?(Rails::VERSION) && Rails::VERSION::MAJOR >= 8
|
20
|
+
# Rails 8 removed ActiveRecord::Migration::CheckPending from the stack
|
21
|
+
app.middleware.use RailsFields::EnforceFieldsMiddleware
|
22
|
+
else
|
23
|
+
begin
|
24
|
+
app.middleware.insert_after ActiveRecord::Migration::CheckPending, RailsFields::EnforceFieldsMiddleware
|
25
|
+
rescue StandardError
|
26
|
+
# Fallback for environments where insert_after target is unavailable
|
27
|
+
app.middleware.use RailsFields::EnforceFieldsMiddleware
|
28
|
+
end
|
29
|
+
end
|
18
30
|
end
|
19
31
|
end
|
20
|
-
end
|
32
|
+
end
|
@@ -1,6 +1,8 @@
|
|
1
1
|
module RailsFields
|
2
2
|
module Utils
|
3
3
|
class << self
|
4
|
+
# Minimal wrapper to represent associations in changes
|
5
|
+
AssociationRef = Struct.new(:name)
|
4
6
|
def allowed_types
|
5
7
|
# TODO: this may depend on the current database adapter or mapper
|
6
8
|
ActiveRecord::Base.connection.native_database_types.keys
|
@@ -35,7 +37,11 @@ module RailsFields
|
|
35
37
|
# @param model [ActiveRecord::Base] the model to check
|
36
38
|
# @return [Hash, Nil] the changes detected
|
37
39
|
def detect_changes(model)
|
38
|
-
|
40
|
+
# Exclude the primary key (e.g., :id) from comparisons
|
41
|
+
primary_key = model.primary_key&.to_sym
|
42
|
+
previous_fields = model.attribute_types
|
43
|
+
.to_h { |k, v| [k.to_sym, v.type] }
|
44
|
+
.reject { |name, _| name == primary_key }
|
39
45
|
declared_fields = model.declared_fields.to_h do |f|
|
40
46
|
[f.name.to_sym, {
|
41
47
|
name: f.type.to_sym,
|
@@ -61,8 +67,7 @@ module RailsFields
|
|
61
67
|
type_name = type[:name]
|
62
68
|
if previous_fields[name]
|
63
69
|
if previous_fields[name] != type_name
|
64
|
-
model_changes[:type_changed] << { name:, from: previous_fields[name],
|
65
|
-
to: type }
|
70
|
+
model_changes[:type_changed] << { name:, from: previous_fields[name], to: type }
|
66
71
|
end
|
67
72
|
else
|
68
73
|
model_changes[:added] << { name:, type: }
|
@@ -89,8 +94,11 @@ module RailsFields
|
|
89
94
|
# Detect potential renames
|
90
95
|
potential_renames = []
|
91
96
|
model_changes[:removed].each do |removed_field|
|
92
|
-
#
|
93
|
-
added_field = model_changes[:added].find
|
97
|
+
# Match by type name (normalize Hash/Scalar)
|
98
|
+
added_field = model_changes[:added].find do |f|
|
99
|
+
added_type = f[:type].is_a?(Hash) ? f[:type][:name] : f[:type]
|
100
|
+
added_type == removed_field[:type]
|
101
|
+
end
|
94
102
|
if added_field
|
95
103
|
potential_renames << { from: removed_field[:name],
|
96
104
|
to: added_field[:name] }
|
@@ -103,8 +111,8 @@ module RailsFields
|
|
103
111
|
|
104
112
|
# Filter out incorrect renames (one-to-one mapping)
|
105
113
|
potential_renames.each do |rename|
|
106
|
-
next unless model_changes[:added].count { |f| f[:
|
107
|
-
model_changes[:removed].count { |f| f[:
|
114
|
+
next unless model_changes[:added].count { |f| f[:name] == rename[:to].to_sym } == 1 &&
|
115
|
+
model_changes[:removed].count { |f| f[:name] == rename[:from].to_sym } == 1
|
108
116
|
|
109
117
|
model_changes[:renamed] << rename
|
110
118
|
model_changes[:added].reject! { |f| f[:name] == rename[:to].to_sym }
|
@@ -117,15 +125,20 @@ module RailsFields
|
|
117
125
|
end
|
118
126
|
|
119
127
|
declared_foreign_keys = declared_associations.map(&:foreign_key).map(&:to_sym)
|
120
|
-
existing_foreign_keys = ActiveRecord::Base.connection
|
128
|
+
existing_foreign_keys = ActiveRecord::Base.connection
|
129
|
+
.foreign_keys(model.table_name)
|
130
|
+
.map { |fk| fk.respond_to?(:column) ? fk.column.to_sym : fk.options[:column].to_sym }
|
121
131
|
|
122
132
|
associations_added = declared_associations.select do |reflection|
|
123
133
|
!existing_foreign_keys.include?(reflection.foreign_key.to_sym)
|
124
134
|
end
|
125
135
|
|
126
|
-
associations_removed = existing_foreign_keys
|
127
|
-
!declared_foreign_keys.include?(foreign_key)
|
128
|
-
|
136
|
+
associations_removed = existing_foreign_keys
|
137
|
+
.select { |foreign_key| !declared_foreign_keys.include?(foreign_key) }
|
138
|
+
.map do |foreign_key|
|
139
|
+
model.reflections.values.find { |reflection| reflection.foreign_key == foreign_key.to_s } ||
|
140
|
+
AssociationRef.new(foreign_key.to_s.delete_suffix('_id').to_sym)
|
141
|
+
end
|
129
142
|
|
130
143
|
model_changes[:associations_added] = associations_added
|
131
144
|
model_changes[:associations_removed] = associations_removed
|
@@ -151,34 +164,34 @@ module RailsFields
|
|
151
164
|
field_type = change[:type]
|
152
165
|
field_type_for_db = field_type[:name]
|
153
166
|
# TODO: custom mapper
|
154
|
-
migration_code << " add_column :#{
|
167
|
+
migration_code << " add_column :#{model.table_name}, :#{change[:name]}, :#{field_type_for_db}"
|
155
168
|
end
|
156
169
|
|
157
170
|
# Handle added associations
|
158
171
|
model_changes.dig(:associations_added)&.each do |assoc|
|
159
|
-
migration_code << " add_reference :#{
|
172
|
+
migration_code << " add_reference :#{model.table_name}, :#{assoc.name}, foreign_key: true"
|
160
173
|
end
|
161
174
|
|
162
175
|
# Handle removed associations
|
163
176
|
model_changes.dig(:associations_removed)&.each do |assoc|
|
164
|
-
migration_code << " remove_reference :#{
|
177
|
+
migration_code << " remove_reference :#{model.table_name}, :#{assoc.name}, foreign_key: true"
|
165
178
|
end
|
166
179
|
|
167
180
|
# Handle removed fields
|
168
181
|
model_changes.dig(:removed)&.each do |change|
|
169
|
-
migration_code << " remove_column :#{
|
182
|
+
migration_code << " remove_column :#{model.table_name}, :#{change[:name]}"
|
170
183
|
end
|
171
184
|
|
172
185
|
# Handle renamed fields
|
173
186
|
model_changes.dig(:renamed)&.each do |change|
|
174
187
|
change_to = change[:to]
|
175
|
-
migration_code << " rename_column :#{
|
188
|
+
migration_code << " rename_column :#{model.table_name}, :#{change[:from]}, :#{change_to}"
|
176
189
|
end
|
177
190
|
|
178
191
|
# Handle fields' type changes
|
179
192
|
model_changes.dig(:type_changed)&.each do |change|
|
180
|
-
change_to = change[:to]
|
181
|
-
migration_code << " change_column :#{
|
193
|
+
change_to = change[:to][:name]
|
194
|
+
migration_code << " change_column :#{model.table_name}, :#{change[:name]}, :#{change_to}"
|
182
195
|
end
|
183
196
|
|
184
197
|
migration_code << " end"
|
@@ -1,29 +1,32 @@
|
|
1
1
|
module RailsFields
|
2
2
|
module Utils
|
3
|
-
#
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
3
|
+
# Define maps only if GraphQL is available
|
4
|
+
if defined?(GraphQL)
|
5
|
+
# TODO: mapper can be different or custom
|
6
|
+
GQL_TO_RAILS_TYPE_MAP = {
|
7
|
+
::GraphQL::Types::String => :string,
|
8
|
+
::GraphQL::Types::Int => :integer,
|
9
|
+
::GraphQL::Types::Float => :float,
|
10
|
+
::GraphQL::Types::Boolean => :boolean,
|
11
|
+
::GraphQL::Types::ID => :integer, # or :string depending on how you handle IDs
|
12
|
+
::GraphQL::Types::ISO8601DateTime => :datetime,
|
13
|
+
::GraphQL::Types::ISO8601Date => :date,
|
14
|
+
::GraphQL::Types::JSON => :json,
|
15
|
+
::GraphQL::Types::BigInt => :bigint
|
16
|
+
}.freeze
|
15
17
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
18
|
+
RAILS_TO_GQL_TYPE_MAP = {
|
19
|
+
# id: ::GraphQL::Types::String,
|
20
|
+
string: ::GraphQL::Types::String,
|
21
|
+
integer: ::GraphQL::Types::Int,
|
22
|
+
float: ::GraphQL::Types::Float,
|
23
|
+
boolean: ::GraphQL::Types::Boolean,
|
24
|
+
datetime: ::GraphQL::Types::ISO8601DateTime,
|
25
|
+
date: ::GraphQL::Types::ISO8601Date,
|
26
|
+
json: ::GraphQL::Types::JSON,
|
27
|
+
bigint: ::GraphQL::Types::BigInt,
|
28
|
+
text: ::GraphQL::Types::String
|
29
|
+
}.freeze
|
30
|
+
end
|
28
31
|
end
|
29
32
|
end
|
data/lib/rails_fields/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rails-fields
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.3.
|
4
|
+
version: 0.3.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Gaston Morixe
|
8
|
-
autorequire:
|
9
8
|
bindir: bin
|
10
9
|
cert_chain: []
|
11
|
-
date:
|
10
|
+
date: 2025-08-21 00:00:00.000000000 Z
|
12
11
|
dependencies:
|
13
12
|
- !ruby/object:Gem::Dependency
|
14
13
|
name: rails
|
@@ -25,7 +24,7 @@ dependencies:
|
|
25
24
|
- !ruby/object:Gem::Version
|
26
25
|
version: '5.0'
|
27
26
|
- !ruby/object:Gem::Dependency
|
28
|
-
name:
|
27
|
+
name: sqlite3
|
29
28
|
requirement: !ruby/object:Gem::Requirement
|
30
29
|
requirements:
|
31
30
|
- - ">="
|
@@ -39,21 +38,7 @@ dependencies:
|
|
39
38
|
- !ruby/object:Gem::Version
|
40
39
|
version: '0'
|
41
40
|
- !ruby/object:Gem::Dependency
|
42
|
-
name:
|
43
|
-
requirement: !ruby/object:Gem::Requirement
|
44
|
-
requirements:
|
45
|
-
- - ">="
|
46
|
-
- !ruby/object:Gem::Version
|
47
|
-
version: '0'
|
48
|
-
type: :development
|
49
|
-
prerelease: false
|
50
|
-
version_requirements: !ruby/object:Gem::Requirement
|
51
|
-
requirements:
|
52
|
-
- - ">="
|
53
|
-
- !ruby/object:Gem::Version
|
54
|
-
version: '0'
|
55
|
-
- !ruby/object:Gem::Dependency
|
56
|
-
name: jekyll
|
41
|
+
name: graphql
|
57
42
|
requirement: !ruby/object:Gem::Requirement
|
58
43
|
requirements:
|
59
44
|
- - ">="
|
@@ -67,7 +52,7 @@ dependencies:
|
|
67
52
|
- !ruby/object:Gem::Version
|
68
53
|
version: '0'
|
69
54
|
- !ruby/object:Gem::Dependency
|
70
|
-
name:
|
55
|
+
name: yard
|
71
56
|
requirement: !ruby/object:Gem::Requirement
|
72
57
|
requirements:
|
73
58
|
- - ">="
|
@@ -92,6 +77,7 @@ extra_rdoc_files: []
|
|
92
77
|
files:
|
93
78
|
- README.md
|
94
79
|
- lib/rails-fields.rb
|
80
|
+
- lib/rails/fields.rb
|
95
81
|
- lib/rails_fields/class_methods.rb
|
96
82
|
- lib/rails_fields/enforce_fields_middleware.rb
|
97
83
|
- lib/rails_fields/errors/rails_fields_error.rb
|
@@ -110,7 +96,6 @@ metadata:
|
|
110
96
|
homepage_uri: https://rails-fields.dev
|
111
97
|
source_code_uri: https://github.com/gastonmorixe/rails-fields
|
112
98
|
bug_tracker_uri: https://github.com/gastonmorixe/rails-fields/issues
|
113
|
-
post_install_message:
|
114
99
|
rdoc_options: []
|
115
100
|
require_paths:
|
116
101
|
- lib
|
@@ -125,8 +110,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
125
110
|
- !ruby/object:Gem::Version
|
126
111
|
version: '0'
|
127
112
|
requirements: []
|
128
|
-
rubygems_version: 3.
|
129
|
-
signing_key:
|
113
|
+
rubygems_version: 3.7.1
|
130
114
|
specification_version: 4
|
131
115
|
summary: Enforce field types and attributes for ActiveRecord models in Ruby on Rails
|
132
116
|
applications.
|