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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4b352b59432076c4b334a34bf061c94ec6982a5eca47adcbcdc58523efadef39
4
- data.tar.gz: bcfe7ba51edb7381f5606bd0130a805a63b2f5a07f59776c0e4c541f5ecef6fc
3
+ metadata.gz: 72386aef5bd4ef9136597eb8226759ce716deb2d4a899b27837031c3d4678596
4
+ data.tar.gz: b0a8d07705dfba50be8270da36854db49cedc9d2c10f09b292cc48674f059965
5
5
  SHA512:
6
- metadata.gz: 000acd3ebbd40351f1270a040cff06eaa5da891c98f22b624b65bb03ef6f2bc32d649d0f9031c5ed7ef454b7484782968bfb47cfae68c0973facf51cf2ce10b6
7
- data.tar.gz: 15fd7d0f3483a43055e17bdbdd3bb12b17509e3bead773b8340b0fb8c5534649f3e66bdbfdfbd48e3004879309c7911ef9cc16396516eed7dadaec7eb9c5ea5d
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
- [![Gem Version](https://badge.fury.io/rb/rails-fields.svg)](https://badge.fury.io/rb/rails-fields)
5
+ [![Gem Version](https://badge.fury.io/rb/rails-fields.svg?v=320)](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
- - 🚀 Auto-magic ActiveRecord **Migrations** generation
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)
@@ -0,0 +1,3 @@
1
+ # Autoload shim for hyphenated gem name.
2
+ # RubyGems will try to require 'rails/fields' for 'rails-fields'.
3
+ require "rails-fields"
@@ -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 << OpenStruct.new(name: name.to_s, type:, null:, index:)
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
- type = Class.new(::Types::BaseObject) do
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
- field_gql_type = f.name == :id ? GraphQL::Types::ID : Utils::RAILS_TO_GQL_TYPE_MAP[f.type]
59
- field f.name, field_gql_type
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
 
@@ -14,7 +14,19 @@ module RailsFields
14
14
  end
15
15
 
16
16
  initializer "rails_fields.middleware" do |app|
17
- app.middleware.insert_before ActiveRecord::Migration::CheckPending, RailsFields::EnforceFieldsMiddleware
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
- previous_fields = model.attribute_types.to_h { |k, v| [k.to_sym, v.type] }
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
- # puts "Log: removed_field: #{removed_field}"
93
- added_field = model_changes[:added].find { |f| f[:type] == removed_field[:type] }
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[:type] == rename[:to].to_sym } == 1 &&
107
- model_changes[:removed].count { |f| f[:type] == rename[:from].to_sym } == 1
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.foreign_keys(model.table_name).map(&:options).map { |opt| opt[:column].to_sym }
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.select do |foreign_key|
127
- !declared_foreign_keys.include?(foreign_key)
128
- end.map { |foreign_key| model.reflections.values.find { |reflection| reflection.foreign_key == foreign_key.to_s } }
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 :#{model_name.tableize}, :#{change[:name]}, :#{field_type_for_db}"
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 :#{model_name.tableize}, :#{assoc.name}, foreign_key: true"
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 :#{model_name.tableize}, :#{assoc.name}, foreign_key: true"
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 :#{model_name.tableize}, :#{change[:name]}"
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 :#{model_name.tableize}, :#{change[:from]}, :#{change_to}"
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 :#{model_name.tableize}, :#{change[:name]}, :#{change_to}"
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
- # TODO: mapper can be different or custom
4
- GQL_TO_RAILS_TYPE_MAP = {
5
- ::GraphQL::Types::String => :string,
6
- ::GraphQL::Types::Int => :integer,
7
- ::GraphQL::Types::Float => :float,
8
- ::GraphQL::Types::Boolean => :boolean,
9
- ::GraphQL::Types::ID => :integer, # or :string depending on how you handle IDs
10
- ::GraphQL::Types::ISO8601DateTime => :datetime,
11
- ::GraphQL::Types::ISO8601Date => :date,
12
- ::GraphQL::Types::JSON => :json,
13
- ::GraphQL::Types::BigInt => :bigint
14
- }.freeze
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
- RAILS_TO_GQL_TYPE_MAP = {
17
- # id: ::GraphQL::Types::String,
18
- string: ::GraphQL::Types::String,
19
- integer: ::GraphQL::Types::Int,
20
- float: ::GraphQL::Types::Float,
21
- boolean: ::GraphQL::Types::Boolean,
22
- datetime: ::GraphQL::Types::ISO8601DateTime,
23
- date: ::GraphQL::Types::ISO8601Date,
24
- json: ::GraphQL::Types::JSON,
25
- bigint: ::GraphQL::Types::BigInt,
26
- text: ::GraphQL::Types::String
27
- }.freeze
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
@@ -1,3 +1,3 @@
1
1
  module RailsFields
2
- VERSION = "0.3.1"
2
+ VERSION = "0.3.3"
3
3
  end
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.1
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: 2023-08-28 00:00:00.000000000 Z
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: graphql
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: yard
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: webrick
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.4.19
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.