propel_api 0.1.1 → 0.1.2

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: 39e7f4915254a37e32cd3ceacaac8402a3c0f32768d0169345cf69fdb742d52d
4
- data.tar.gz: 302f7022de04cd30f8c0efac85267a4e636c175169478600094a7f9c9100c465
3
+ metadata.gz: 53bb582cea07d5d549c8ebc4468a4ad78b8724bd68a47b0d790c210ca9ab5d67
4
+ data.tar.gz: 8bfafc7d21cc11e1ad7e189239085af3738f907221793c928dd99bc2ea0f7594
5
5
  SHA512:
6
- metadata.gz: f74c378babcf34e7077117ae21720335903589c58e4aeb68800b3abbfe8c621c323c1f0b69d12557e80e27b3c715c9aa5047a8b9c7389c834db884435e6e4cec
7
- data.tar.gz: 6e93710aad4c65df5822e046239b1ea1a661dd44d7efdef050fd4fcfd734a216e4451150513df75d71f7d21748cabf83d9eb70eced4651270ff8bfa27f230e35
6
+ metadata.gz: 9d96190183561db7fbbcd218fafac7e9939bc409b44ab1d4f6fa6795cbfe2ddd99890336fdb277e8bd13487e8388bb26bb38c0592c4b15e2e02a8d2745cb72df
7
+ data.tar.gz: 0d9f7d831291a748e296391e727eadb33e738054c028d664dde2791b029ac5963550253b7b33d5ffd753e7314c32755d190b49c541c7d2e39b6e35e1cbee674b
data/CHANGELOG.md CHANGED
@@ -9,10 +9,47 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
  ### Planned Features
11
11
  - GraphQL adapter support
12
- - API versioning migration tools
13
- - Advanced relationship inference patterns
14
- - Custom serialization adapter framework
15
- - Performance optimization tools
12
+
13
+ ## [0.1.2] - 2025-01-11
14
+
15
+ ### Added
16
+ - **Automatic inverse relationship generation** - When creating resources with references, parent models are automatically updated
17
+ - `has_many :children, dependent: :destroy` relationships automatically added to existing parent models
18
+ - Smart detection of existing associations to prevent duplication
19
+ - Polymorphic association support with proper inverse relationship handling
20
+ - Clear console feedback showing which relationships were added
21
+ - Graceful handling of non-existent parent models
22
+
23
+ - **Enhanced relationship inference system**
24
+ - Improved `RelationshipInferrer` class with comprehensive association detection
25
+ - Support for both standard (`user:references`) and polymorphic (`commentable:references`, `resource_parent:references`) patterns
26
+ - Intelligent model file modification with proper placement of associations
27
+ - Preservation of existing model structure and comments
28
+
29
+ - **Optional usage comments in controllers** - `--with-comments` flag for generators
30
+ - Clean controllers by default (production-ready, ~7-8 lines)
31
+ - Detailed usage comments when `--with-comments` flag is used (~80+ lines with examples)
32
+ - Available for both `propel_api:controller` and `propel_api:resource` generators
33
+ - Works with both PropelFacets and Graphiti adapters
34
+
35
+ ### Fixed
36
+ - **Association generation respects `--skip-associations` flag** - Fixed template fallback logic
37
+ - **Polymorphic associations correctly identified** - No inverse relationships created for polymorphic references
38
+ - **Model template conditional logic** - Improved ERB template structure for better flag handling
39
+
40
+ ### Improved
41
+ - **Comprehensive test coverage** - 16 new tests for relationship inference scenarios
42
+ - **Better error handling** - Graceful recovery when parent model modifications fail
43
+ - **Console output** - Color-coded feedback for relationship additions and warnings
44
+
45
+ ## [0.1.1] - 2025-01-10
46
+
47
+ ### Added
48
+ - **Optional usage comments in controllers** - `--with-comments` flag for generators
49
+ - Clean controllers by default (production-ready)
50
+ - Detailed usage comments when `--with-comments` flag is used
51
+ - Available for both `propel_api:controller` and `propel_api:resource` generators
52
+ - Works with both PropelFacets and Graphiti adapters
16
53
 
17
54
  ## [0.1.0] - 2025-01-XX
18
55
 
@@ -43,6 +43,11 @@ module PropelApi
43
43
  desc "Generate PropelApi controller, routes and tests for existing model"
44
44
 
45
45
  argument :attributes, type: :array, default: [], banner: "field:type field:type"
46
+
47
+ class_option :with_comments,
48
+ type: :boolean,
49
+ default: false,
50
+ desc: "Include usage comments in generated controller"
46
51
 
47
52
  def validate_model_exists
48
53
  # Ensure attributes are parsed before validation
@@ -61,7 +61,7 @@ class RelationshipInferrer
61
61
  reference_name = attribute.name.to_s
62
62
  class_name = reference_name.camelize
63
63
 
64
- # Add inverse relationship
64
+ # Add inverse relationship for non-polymorphic references only
65
65
  add_inverse_relationship(class_name, standard_has_many)
66
66
 
67
67
  # All references except organization are optional by default (PropelAPI convention)
@@ -38,6 +38,11 @@ module PropelApi
38
38
  class_option :skip_tenancy,
39
39
  type: :boolean,
40
40
  desc: "Skip multi-tenancy foundation (organization and agency)"
41
+
42
+ class_option :with_comments,
43
+ type: :boolean,
44
+ default: false,
45
+ desc: "Include usage comments in generated controller"
41
46
 
42
47
  def create_migration
43
48
  initialize_propel_api_settings
@@ -112,6 +117,9 @@ module PropelApi
112
117
  else
113
118
  raise "Unknown adapter: #{@adapter}. Run 'rails generate propel_api:install' first."
114
119
  end
120
+
121
+ # Apply inverse relationships to existing parent models
122
+ apply_inverse_relationships
115
123
  end
116
124
 
117
125
  def create_controller
@@ -248,6 +256,99 @@ module PropelApi
248
256
  @relationship_inferrer
249
257
  end
250
258
 
259
+ def apply_inverse_relationships
260
+ return unless @relationship_inferrer.should_generate_associations?
261
+ return if behavior == :revoke
262
+
263
+ # Get the inverse relationships from the inferrer
264
+ inverse_relationships = @relationship_inferrer.inverse_relationships
265
+
266
+ inverse_relationships.each do |parent_class_name, relationships|
267
+ parent_model_file = "app/models/#{parent_class_name.underscore}.rb"
268
+ full_path = File.join(destination_root, parent_model_file)
269
+
270
+ # Skip if parent model doesn't exist
271
+ next unless File.exist?(full_path)
272
+
273
+ # Skip polymorphic relationships (they don't get inverse relationships)
274
+ # Only standard belongs_to relationships get has_many inverses
275
+ next if relationships.any? { |rel| rel.include?('polymorphic') }
276
+
277
+ begin
278
+ update_parent_model(full_path, relationships, parent_class_name)
279
+ rescue => e
280
+ say "Warning: Could not update #{parent_model_file}: #{e.message}", :yellow
281
+ end
282
+ end
283
+ end
284
+
285
+ def update_parent_model(model_file_path, relationships, parent_class_name)
286
+ content = File.read(model_file_path)
287
+ original_content = content.dup
288
+
289
+ relationships.each do |relationship|
290
+ # Check if relationship already exists to avoid duplication
291
+ if content.match?(/^\s*#{Regexp.escape(relationship)}\s*$/)
292
+ say " #{parent_class_name} already has: #{relationship}", :blue
293
+ next
294
+ end
295
+
296
+ # Find the associations section and add the new relationship
297
+ content = add_relationship_to_model(content, relationship, parent_class_name)
298
+ end
299
+
300
+ # Only write if content changed
301
+ if content != original_content
302
+ File.write(model_file_path, content)
303
+ relationships.each do |relationship|
304
+ say " ✅ Added to #{parent_class_name}: #{relationship}", :green
305
+ end
306
+ end
307
+ end
308
+
309
+ def add_relationship_to_model(content, relationship, parent_class_name)
310
+ # Look for existing associations section patterns
311
+ association_patterns = [
312
+ /^(\s*# Associations\s*\n)/,
313
+ /^(\s*belongs_to\s+.*\n)/,
314
+ /^(\s*has_many\s+.*\n)/,
315
+ /^(\s*has_one\s+.*\n)/
316
+ ]
317
+
318
+ # Try to find existing associations section
319
+ association_match = nil
320
+ association_patterns.each do |pattern|
321
+ match = content.match(pattern)
322
+ if match
323
+ association_match = match
324
+ break
325
+ end
326
+ end
327
+
328
+ if association_match
329
+ # Insert after the associations section
330
+ insertion_point = association_match.end(0)
331
+ content.insert(insertion_point, " #{relationship}\n")
332
+ else
333
+ # Look for the class declaration and add associations section
334
+ class_match = content.match(/^class\s+#{Regexp.escape(parent_class_name)}\s*<.*\n/)
335
+ if class_match
336
+ insertion_point = class_match.end(0)
337
+ associations_section = "\n # Associations\n #{relationship}\n"
338
+ content.insert(insertion_point, associations_section)
339
+ else
340
+ # Fallback: add before the end of the class
341
+ end_match = content.match(/^end\s*$/)
342
+ if end_match
343
+ insertion_point = end_match.begin(0)
344
+ content.insert(insertion_point, " # Associations\n #{relationship}\n\n")
345
+ end
346
+ end
347
+ end
348
+
349
+ content
350
+ end
351
+
251
352
  # Override Base class method to enable Graphiti resource generation for main generator
252
353
  def should_generate_graphiti_resource?
253
354
  true
@@ -5,6 +5,7 @@ class <%= api_controller_class_name %> < ApplicationController
5
5
 
6
6
  before_action :authenticate_user
7
7
 
8
+ <% if options[:with_comments] -%>
8
9
  # Graphiti handles CRUD operations through resources automatically
9
10
  # Individual controllers should define their resource class like:
10
11
  # class UsersController < <%= api_controller_class_name %>
@@ -14,6 +15,7 @@ class <%= api_controller_class_name %> < ApplicationController
14
15
  # Resources should be created using:
15
16
  # rails generate graphiti:resource User
16
17
 
18
+ <% end -%>
17
19
  def index
18
20
  resources = resource.all(params)
19
21
  respond_with(resources)
@@ -53,17 +55,21 @@ class <%= api_controller_class_name %> < ApplicationController
53
55
 
54
56
  private
55
57
 
58
+ <% if options[:with_comments] -%>
56
59
  # Override in subclasses to specify the Graphiti resource class
57
60
  # Example:
58
61
  # def resource
59
62
  # UserResource
60
63
  # end
64
+ <% end -%>
61
65
  def resource
62
66
  @resource ||= infer_resource_class.new
63
67
  end
64
68
 
69
+ <% if options[:with_comments] -%>
65
70
  # Auto-infer resource class from controller name
66
71
  # e.g., UsersController -> UserResource
72
+ <% end -%>
67
73
  def infer_resource_class
68
74
  resource_class_name = "#{controller_name.classify}Resource"
69
75
  resource_class_name.constantize
@@ -71,9 +77,11 @@ class <%= api_controller_class_name %> < ApplicationController
71
77
  raise "#{resource_class_name} not found. Please create it using: rails generate graphiti:resource #{controller_name.classify}"
72
78
  end
73
79
 
80
+ <% if options[:with_comments] -%>
74
81
  # Graphiti handles parameter filtering through resources automatically
75
82
  # No need for manual strong parameters - resources define allowed attributes
76
83
  # Define attributes in your resource files:
77
84
  # attribute :name, :string
78
85
  # attribute :email, :string
86
+ <% end -%>
79
87
  end
@@ -8,7 +8,9 @@ class <%= api_controller_class_name %> < ApplicationController
8
8
  before_action :authenticate_user
9
9
  before_action :set_resource, only: [:show, :update, :destroy]
10
10
 
11
+ <% if options[:with_comments] -%>
11
12
  # Connect default facets to actions - can be overridden in subclasses
13
+ <% end -%>
12
14
  connect_facet :short, actions: [:index]
13
15
  connect_facet :details, actions: [:show, :update, :create]
14
16
 
@@ -56,6 +58,7 @@ class <%= api_controller_class_name %> < ApplicationController
56
58
  @resource = resource_class.find(params[:id])
57
59
  end
58
60
 
61
+ <% if options[:with_comments] -%>
59
62
  # resource_params method is provided by StrongParamsHelper concern
60
63
  # Define permitted parameters in your individual controllers using:
61
64
  # permitted_params :field1, :field2, :field3
@@ -63,6 +66,7 @@ class <%= api_controller_class_name %> < ApplicationController
63
66
  # Example for a UsersController:
64
67
  # permitted_params :username, :email_address, :first_name, :last_name
65
68
 
69
+ <% end -%>
66
70
  def pagy_metadata(pagy)
67
71
  {
68
72
  current_page: pagy.page,
@@ -1,10 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class <%= controller_class_name_with_namespace %>Controller < <%= api_controller_class_name %>
4
+ <% if options[:with_comments] -%>
4
5
  # Define which parameters are allowed for <%= class_name %> creation/updates
5
6
  # This uses the StrongParamsHelper concern from the base controller
7
+ <% end -%>
6
8
  permitted_params <%= permitted_param_names.map { |attribute| ":#{attribute}" }.join(', ') %>
7
9
 
10
+ <% if options[:with_comments] -%>
8
11
  # Connect facets to actions - customize as needed for your <%= class_name.downcase %> model
9
12
  # Default facets (:short for index, :details for show/create/update) are inherited from parent
10
13
  # Uncomment and customize these lines if you want different facet connections:
@@ -77,4 +80,6 @@ class <%= controller_class_name_with_namespace %>Controller < <%= api_controller
77
80
  # authorize! :read, @resource # Example: authorization check
78
81
  # render json: { data: resource_json(@resource) }
79
82
  # end
83
+
84
+ <% end -%>
80
85
  end
@@ -58,7 +58,7 @@ class <%= class_name %> < ApplicationRecord
58
58
  <%= relationship %>
59
59
  <% end -%>
60
60
  <% end -%>
61
- <% else -%>
61
+ <% elsif !options[:skip_associations] -%>
62
62
  <% attributes.select { |attr| attr.type == :references }.each do |attribute| -%>
63
63
  <% # Skip organization and agency if tenancy is included (they're already added above) -%>
64
64
  <% unless !options[:skip_tenancy] && (attribute.name == 'organization' || attribute.name == 'agency') -%>
@@ -1,9 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class <%= controller_class_name_with_namespace %>Controller < <%= api_controller_class_name %>
4
+ <% if options[:with_comments] -%>
4
5
  # Specify the Graphiti resource class for this controller
6
+ <% end -%>
5
7
  self.resource = <%= class_name %>Resource
6
8
 
9
+ <% if options[:with_comments] -%>
7
10
  # All CRUD methods (index, show, create, update, destroy) are inherited from <%= api_controller_class_name %>
8
11
  # The inherited methods provide:
9
12
  # - JSON:API compliant responses
@@ -63,8 +66,10 @@ class <%= controller_class_name_with_namespace %>Controller < <%= api_controller
63
66
  # respond_with(resource_instance)
64
67
  # end
65
68
 
69
+ <% end -%>
66
70
  private
67
71
 
72
+ <% if options[:with_comments] -%>
68
73
  # Alternative: Override the resource method instead of using self.resource
69
74
  # def resource
70
75
  # <%= class_name %>Resource
@@ -79,4 +84,5 @@ class <%= controller_class_name_with_namespace %>Controller < <%= api_controller
79
84
  # def additional_<%= singular_table_name %>_setup
80
85
  # # Custom setup logic
81
86
  # end
87
+ <% end -%>
82
88
  end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ <% unless options[:skip_tenancy] -%>
4
+ require_relative '../concerns/tenancy'
5
+ <% end -%>
6
+
7
+ ##
8
+ # Graphiti Resource for <%= class_name %>
9
+ #
10
+ # Provides API serialization, filtering, and pagination for <%= class_name %> models.
11
+ # Generated by PropelApi with Graphiti adapter.
12
+ #
13
+ # @example Usage
14
+ # # In your controller:
15
+ # def index
16
+ # <%= plural_name %> = <%= class_name %>Resource.all(params)
17
+ # render jsonapi: <%= plural_name %>
18
+ # end
19
+ #
20
+ # @see https://www.graphiti.dev/guides/concepts/resources
21
+ #
22
+ class <%= class_name %>Resource < ApplicationResource
23
+ <% unless options[:skip_tenancy] -%>
24
+ include Tenancy
25
+ <% end -%>
26
+
27
+ # Define the associated model
28
+ self.model = <%= class_name %>
29
+
30
+ # Define available attributes for serialization
31
+ <% attributes.each do |attribute| -%>
32
+ <% next if attribute.name == 'id' -%>
33
+ <% if attribute.type == :references -%>
34
+ attribute :<%= attribute.name %>_id, :integer
35
+ <% else -%>
36
+ attribute :<%= attribute.name %>, :<%= attribute.type %>
37
+ <% end -%>
38
+ <% end -%>
39
+
40
+ # Define filterable attributes
41
+ filter :id, :integer
42
+ <% attributes.each do |attribute| -%>
43
+ <% next if attribute.name == 'id' -%>
44
+ <% if attribute.type == :references -%>
45
+ filter :<%= attribute.name %>_id, :integer
46
+ <% else -%>
47
+ filter :<%= attribute.name %>, :<%= attribute.type %>
48
+ <% end -%>
49
+ <% end -%>
50
+
51
+ # Define sortable attributes
52
+ sort_all
53
+
54
+ # Define default sort
55
+ default_sort :id
56
+
57
+ # Define pagination
58
+ page_size 25
59
+ max_page_size 100
60
+
61
+ <% if defined?(relationship_inferrer) && relationship_inferrer.should_generate_associations? -%>
62
+ # Relationships
63
+ <% relationship_inferrer.belongs_to_relationships.each do |relationship| -%>
64
+ <% # Extract the association name from the relationship string -%>
65
+ <% association_name = relationship.match(/belongs_to :(\w+)/)[1] -%>
66
+ belongs_to :<%= association_name %>, resource: <%= association_name.camelize %>Resource
67
+ <% end -%>
68
+ <% end -%>
69
+
70
+ # Define scopes for filtering
71
+ scope :all, :all
72
+
73
+ # Custom scopes can be added here
74
+ # scope :active, -> { where(active: true) }
75
+
76
+ private
77
+
78
+ # Override to add custom query logic
79
+ def base_scope
80
+ super
81
+ end
82
+
83
+ # Override to add custom authorization logic
84
+ def resolve(scope)
85
+ super
86
+ end
87
+ end
data/lib/propel_api.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module PropelApi
2
- VERSION = "0.1.1"
2
+ VERSION = "0.1.2"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: propel_api
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryan Martin, Rafael Pivato, Chi Putera
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-07-11 00:00:00.000000000 Z
11
+ date: 2025-07-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -31,8 +31,8 @@ dependencies:
31
31
  - !ruby/object:Gem::Version
32
32
  version: '9.0'
33
33
  description: A comprehensive Rails generator that creates complete API resources with
34
- models, controllers, tests, and realistic seed data using Faker. Supports both JSON
35
- Facet and Graphiti serialization engines.
34
+ models, controllers, tests, and realistic seed data. Supports both JSON Facet and
35
+ Graphiti serialization engines.
36
36
  email:
37
37
  - admin@propelhq.dev
38
38
  executables: []
@@ -60,6 +60,7 @@ files:
60
60
  - lib/generators/propel_api/templates/scaffold/facet_model_template.rb.tt
61
61
  - lib/generators/propel_api/templates/scaffold/graphiti_controller_template.rb.tt
62
62
  - lib/generators/propel_api/templates/scaffold/graphiti_model_template.rb.tt
63
+ - lib/generators/propel_api/templates/scaffold/graphiti_resource_template.rb.tt
63
64
  - lib/generators/propel_api/templates/seeds/seeds_template.rb.tt
64
65
  - lib/generators/propel_api/templates/tests/controller_test_template.rb.tt
65
66
  - lib/generators/propel_api/templates/tests/fixtures_template.yml.tt
@@ -71,8 +72,9 @@ homepage: https://github.com/propel-hq/propel_rails.git
71
72
  licenses:
72
73
  - MIT
73
74
  metadata:
75
+ allowed_push_host: https://rubygems.org
74
76
  homepage_uri: https://github.com/propel-hq/propel_rails.git
75
- rubygems_mfa_required: 'true'
77
+ source_code_uri: https://github.com/propel-hq/propel_rails.git
76
78
  post_install_message:
77
79
  rdoc_options: []
78
80
  require_paths: