propel_api 0.1.1

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.
Files changed (30) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +59 -0
  3. data/LICENSE +21 -0
  4. data/README.md +320 -0
  5. data/Rakefile +36 -0
  6. data/lib/generators/propel_api/USAGE +8 -0
  7. data/lib/generators/propel_api/controller/controller_generator.rb +208 -0
  8. data/lib/generators/propel_api/core/base.rb +19 -0
  9. data/lib/generators/propel_api/core/configuration_methods.rb +187 -0
  10. data/lib/generators/propel_api/core/named_base.rb +457 -0
  11. data/lib/generators/propel_api/core/path_generation_methods.rb +45 -0
  12. data/lib/generators/propel_api/core/relationship_inferrer.rb +117 -0
  13. data/lib/generators/propel_api/install/install_generator.rb +343 -0
  14. data/lib/generators/propel_api/resource/resource_generator.rb +433 -0
  15. data/lib/generators/propel_api/templates/config/propel_api.rb.tt +149 -0
  16. data/lib/generators/propel_api/templates/controllers/api_controller_graphiti.rb +79 -0
  17. data/lib/generators/propel_api/templates/controllers/api_controller_propel_facets.rb +76 -0
  18. data/lib/generators/propel_api/templates/controllers/example_controller.rb.tt +96 -0
  19. data/lib/generators/propel_api/templates/scaffold/facet_controller_template.rb.tt +80 -0
  20. data/lib/generators/propel_api/templates/scaffold/facet_model_template.rb.tt +141 -0
  21. data/lib/generators/propel_api/templates/scaffold/graphiti_controller_template.rb.tt +82 -0
  22. data/lib/generators/propel_api/templates/scaffold/graphiti_model_template.rb.tt +32 -0
  23. data/lib/generators/propel_api/templates/seeds/seeds_template.rb.tt +493 -0
  24. data/lib/generators/propel_api/templates/tests/controller_test_template.rb.tt +485 -0
  25. data/lib/generators/propel_api/templates/tests/fixtures_template.yml.tt +250 -0
  26. data/lib/generators/propel_api/templates/tests/integration_test_template.rb.tt +487 -0
  27. data/lib/generators/propel_api/templates/tests/model_test_template.rb.tt +252 -0
  28. data/lib/generators/propel_api/unpack/unpack_generator.rb +304 -0
  29. data/lib/propel_api.rb +3 -0
  30. metadata +95 -0
@@ -0,0 +1,76 @@
1
+ class <%= api_controller_class_name %> < ApplicationController
2
+ include HasScope
3
+ include Pagy::Backend
4
+ include FacetRenderer
5
+ include StrongParamsHelper
6
+ include PropelAuthentication
7
+
8
+ before_action :authenticate_user
9
+ before_action :set_resource, only: [:show, :update, :destroy]
10
+
11
+ # Connect default facets to actions - can be overridden in subclasses
12
+ connect_facet :short, actions: [:index]
13
+ connect_facet :details, actions: [:show, :update, :create]
14
+
15
+ def index
16
+ @pagy, resources = pagy(apply_scopes(resource_class.all))
17
+ render json: {
18
+ data: resources.map { |resource| resource_json(resource) },
19
+ pagination: pagy_metadata(@pagy)
20
+ }
21
+ end
22
+
23
+ def show
24
+ render json: { data: resource_json(@resource) }
25
+ end
26
+
27
+ def create
28
+ @resource = resource_class.new(resource_params)
29
+ if @resource.save
30
+ render json: { data: resource_json(@resource) }, status: :created
31
+ else
32
+ render json: { errors: @resource.errors }, status: :unprocessable_entity
33
+ end
34
+ end
35
+
36
+ def update
37
+ if @resource.update(resource_params)
38
+ render json: { data: resource_json(@resource) }
39
+ else
40
+ render json: { errors: @resource.errors }, status: :unprocessable_entity
41
+ end
42
+ end
43
+
44
+ def destroy
45
+ @resource.destroy
46
+ head :no_content
47
+ end
48
+
49
+ private
50
+
51
+ def resource_class
52
+ @resource_class ||= controller_name.classify.constantize
53
+ end
54
+
55
+ def set_resource
56
+ @resource = resource_class.find(params[:id])
57
+ end
58
+
59
+ # resource_params method is provided by StrongParamsHelper concern
60
+ # Define permitted parameters in your individual controllers using:
61
+ # permitted_params :field1, :field2, :field3
62
+ #
63
+ # Example for a UsersController:
64
+ # permitted_params :username, :email_address, :first_name, :last_name
65
+
66
+ def pagy_metadata(pagy)
67
+ {
68
+ current_page: pagy.page,
69
+ per_page: pagy.items,
70
+ total_pages: pagy.pages,
71
+ total_count: pagy.count,
72
+ next_page: pagy.next,
73
+ prev_page: pagy.prev
74
+ }
75
+ end
76
+ end
@@ -0,0 +1,96 @@
1
+ # Example controller showing how to use the generated Api::ApiController
2
+ #
3
+ # This file demonstrates proper usage of both JSON Facet and Graphiti patterns.
4
+ # Choose the pattern that matches your serialization engine selection.
5
+
6
+ <% if @engine == 'json_facet' -%>
7
+ # JSON Facet Controller Example
8
+ # ============================
9
+ # Generate this controller with:
10
+ # rails generate controller api/<%= @resource_name.pluralize %> --no-helper --no-assets --no-view-specs
11
+ #
12
+ # Then replace the generated content with:
13
+
14
+ module Api
15
+ class <%= @resource_name.pluralize.camelize %>Controller < Api::ApiController
16
+ # Define which parameters are allowed for <%= @resource_name %> creation/updates
17
+ # This uses the StrongParamsHelper concern from the base controller
18
+ permitted_params :name, :description, :status # Replace with your <%= @resource_name.downcase %> attributes
19
+
20
+ # Optional: Override facet connections if needed
21
+ # (otherwise uses defaults from parent: :short for index, :details for show/create/update)
22
+ connect_facet :summary, actions: [:index]
23
+ connect_facet :full, actions: [:show, :create, :update]
24
+
25
+ # Optional: Add scopes using has_scope (if using Ransack or similar)
26
+ # has_scope :by_status
27
+ # has_scope :search, using: :name_or_description_cont
28
+
29
+ # All CRUD methods are inherited from Api::ApiController
30
+ # Override them here if you need custom behavior:
31
+ #
32
+ # def index
33
+ # # Custom index logic
34
+ # super
35
+ # end
36
+ #
37
+ # def create
38
+ # # Custom creation logic
39
+ # @resource = resource_class.new(resource_params)
40
+ # @resource.user = current_user # Example: set current user
41
+ #
42
+ # if @resource.save
43
+ # render json: { data: resource_json(@resource) }, status: :created
44
+ # else
45
+ # render json: { errors: @resource.errors }, status: :unprocessable_entity
46
+ # end
47
+ # end
48
+ end
49
+ end
50
+
51
+ <% elsif @engine == 'graphiti' -%>
52
+ # Graphiti Controller Example
53
+ # ===========================
54
+ # 1. First generate the Graphiti resource:
55
+ # rails generate graphiti:resource <%= @resource_name %>
56
+ #
57
+ # 2. Generate this controller with:
58
+ # rails generate controller api/<%= @resource_name.pluralize %> --no-helper --no-assets --no-view-specs
59
+ #
60
+ # 3. Replace the generated content with:
61
+
62
+ module Api
63
+ class <%= @resource_name.pluralize.camelize %>Controller < Api::ApiController
64
+ # Specify the Graphiti resource class for this controller
65
+ self.resource = <%= @resource_name %>Resource
66
+
67
+ # Alternative: Override the resource method instead
68
+ # private
69
+ #
70
+ # def resource
71
+ # <%= @resource_name %>Resource
72
+ # end
73
+
74
+ # All CRUD methods are inherited from Api::ApiController
75
+ # Override them here if you need custom behavior:
76
+ #
77
+ # def create
78
+ # # Custom creation logic before save
79
+ # resource_instance = resource.build(params)
80
+ # resource_instance.data.user = current_user # Example: set current user
81
+ #
82
+ # if resource_instance.save
83
+ # respond_with(resource_instance, status: :created)
84
+ # else
85
+ # respond_with(resource_instance.errors, status: :unprocessable_entity)
86
+ # end
87
+ # end
88
+ end
89
+ end
90
+
91
+ # Don't forget to update your routes in config/routes.rb:
92
+ # namespace :api do
93
+ # resources :<%= @resource_name.pluralize.underscore %>
94
+ # end
95
+
96
+ <% end -%>
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ class <%= controller_class_name_with_namespace %>Controller < <%= api_controller_class_name %>
4
+ # Define which parameters are allowed for <%= class_name %> creation/updates
5
+ # This uses the StrongParamsHelper concern from the base controller
6
+ permitted_params <%= permitted_param_names.map { |attribute| ":#{attribute}" }.join(', ') %>
7
+
8
+ # Connect facets to actions - customize as needed for your <%= class_name.downcase %> model
9
+ # Default facets (:short for index, :details for show/create/update) are inherited from parent
10
+ # Uncomment and customize these lines if you want different facet connections:
11
+ # connect_facet :summary, actions: [:index]
12
+ # connect_facet :full, actions: [:show, :create, :update]
13
+
14
+ # Optional: Add scopes using has_scope (if using Ransack or similar)
15
+ # Uncomment these lines if you have corresponding scopes in your <%= class_name %> model:
16
+ # has_scope :by_status
17
+ # has_scope :search, using: :name_cont
18
+ # has_scope :created_after, type: :date
19
+
20
+ # All CRUD methods (index, show, create, update, destroy) are inherited from <%= api_controller_class_name %>
21
+ # The inherited methods provide:
22
+ # - Automatic pagination via Pagy
23
+ # - Facet-based JSON rendering
24
+ # - Strong parameter filtering
25
+ # - Error handling
26
+ # - Resource finding and creation
27
+
28
+ # Callbacks can be added to run code before or after the default CRUD actions
29
+ # Use standard Rails before_action and after_action callbacks:
30
+ #
31
+ # before_action :your_method_name, only: [:create, :update]
32
+ # after_action :another_method, only: :destroy
33
+ #
34
+ # Example callbacks:
35
+ #
36
+ # before_action :set_defaults, only: :create
37
+ # before_action :check_ownership, only: [:update, :destroy]
38
+ # after_action :notify_users, only: [:create, :update, :destroy]
39
+ #
40
+ # def set_defaults
41
+ # # Set default values before creation
42
+ # end
43
+ #
44
+ # def check_ownership
45
+ # # Verify resource ownership
46
+ # end
47
+ #
48
+ # def notify_users
49
+ # # Send notifications after changes
50
+ # end
51
+
52
+ # Override any inherited methods here as a last result if you need custom behavior that can't be handled by callbacks:
53
+ #
54
+ # def index
55
+ # # Custom index logic (e.g., additional filtering)
56
+ # @pagy, resources = pagy(apply_scopes(resource_class.includes(:association)))
57
+ # render json: {
58
+ # data: resources.map { |resource| resource_json(resource) },
59
+ # pagination: pagy_metadata(@pagy)
60
+ # }
61
+ # end
62
+ #
63
+ # def create
64
+ # # Custom creation logic (e.g., set current user)
65
+ # @resource = resource_class.new(resource_params)
66
+ # @resource.user = current_user # Example: set current user
67
+ #
68
+ # if @resource.save
69
+ # render json: { data: resource_json(@resource) }, status: :created
70
+ # else
71
+ # render json: { errors: @resource.errors }, status: :unprocessable_entity
72
+ # end
73
+ # end
74
+ #
75
+ # def show
76
+ # # Custom show logic (e.g., check permissions)
77
+ # authorize! :read, @resource # Example: authorization check
78
+ # render json: { data: resource_json(@resource) }
79
+ # end
80
+ end
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ class <%= class_name %> < ApplicationRecord
4
+
5
+ # Validations
6
+ <% unless options[:skip_tenancy] -%>
7
+ # Multi-tenancy: Organization is always required
8
+ validates :organization, presence: true
9
+
10
+ <% end -%>
11
+ <% attributes.reject { |attr| attr.type == :references }.each do |attribute| -%>
12
+ <% if attribute.required? -%>
13
+ validates :<%= attribute.name %>, presence: true
14
+ <% end -%>
15
+ <% case attribute.type -%>
16
+ <% when :integer -%>
17
+ validates :<%= attribute.name %>, numericality: { only_integer: true }, allow_nil: <%= !attribute.required? %>
18
+ <% when :decimal, :float -%>
19
+ validates :<%= attribute.name %>, numericality: true, allow_nil: <%= !attribute.required? %>
20
+ <% end -%>
21
+ <% if attribute.name.to_s.match?(/\A(email|email_address)\z/i) -%>
22
+ validates :<%= attribute.name %>, format: { with: URI::MailTo::EMAIL_REGEXP }, allow_nil: <%= !attribute.required? %>
23
+ <% end -%>
24
+ <% if attribute.name.to_s.match?(/\A(phone|phone_number)\z/i) -%>
25
+ validates :<%= attribute.name %>, format: { with: /\A\+?[\d\s-\(\)]+\z/ }, allow_nil: <%= !attribute.required? %>
26
+ <% end -%>
27
+ <% if attribute.name.to_s.match?(/\A(url|website|web_address)\z/i) -%>
28
+ validates :<%= attribute.name %>, format: { with: URI::DEFAULT_PARSER.make_regexp(%w[http https]) }, allow_nil: <%= !attribute.required? %>
29
+ <% end -%>
30
+ <% if attribute.name.to_s.match?(/\Alatitude\z/i) -%>
31
+ validates :<%= attribute.name %>, numericality: { greater_than_or_equal_to: -90, less_than_or_equal_to: 90 }, allow_nil: <%= !attribute.required? %>
32
+ <% end -%>
33
+ <% if attribute.name.to_s.match?(/\Alongitude\z/i) -%>
34
+ validates :<%= attribute.name %>, numericality: { greater_than_or_equal_to: -180, less_than_or_equal_to: 180 }, allow_nil: <%= !attribute.required? %>
35
+ <% end -%>
36
+ <% if attribute.name.to_s.match?(/\A(zip|postal)_code\z/i) -%>
37
+ validates :<%= attribute.name %>, format: { with: /\A\d{5}(-\d{4})?\z/ }, allow_nil: <%= !attribute.required? %>
38
+ <% end -%>
39
+ <% if attribute.name.to_s.match?(/\A(username)\z/i) -%>
40
+ validates :<%= attribute.name %>, format: { with: /\A[a-zA-Z0-9_-]+\z/ }, length: { minimum: 3, maximum: 30 }, allow_nil: <%= !attribute.required? %>
41
+ <% end -%>
42
+ <% if attribute.name.to_s.match?(/\A(ip_address)\z/i) -%>
43
+ validates :<%= attribute.name %>, format: { with: /\A((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\z/ }, allow_nil: <%= !attribute.required? %>
44
+ <% end -%>
45
+ <% end -%>
46
+
47
+ # Associations
48
+ <% unless options[:skip_tenancy] -%>
49
+ # Multi-tenancy: Always include organization (required) and agency (optional)
50
+ belongs_to :organization
51
+ belongs_to :agency, optional: true
52
+
53
+ <% end -%>
54
+ <% if defined?(relationship_inferrer) && relationship_inferrer.should_generate_associations? -%>
55
+ <% relationship_inferrer.belongs_to_relationships.each do |relationship| -%>
56
+ <% # Skip organization and agency if tenancy is included (they're already added above) -%>
57
+ <% unless !options[:skip_tenancy] && (relationship.include?('organization') || relationship.include?('agency')) -%>
58
+ <%= relationship %>
59
+ <% end -%>
60
+ <% end -%>
61
+ <% else -%>
62
+ <% attributes.select { |attr| attr.type == :references }.each do |attribute| -%>
63
+ <% # Skip organization and agency if tenancy is included (they're already added above) -%>
64
+ <% unless !options[:skip_tenancy] && (attribute.name == 'organization' || attribute.name == 'agency') -%>
65
+ <% if attribute.name == 'organization' -%>
66
+ belongs_to :organization
67
+ <% else -%>
68
+ belongs_to :<%= attribute.name %>, optional: true
69
+ <% end -%>
70
+ <% end -%>
71
+ <% end -%>
72
+ <% end -%>
73
+ # Add additional associations here
74
+ # has_many :comments, dependent: :destroy
75
+
76
+ # Scopes (useful for has_scope in controllers)
77
+ # scope :active, -> { where(active: true) }
78
+ # scope :by_status, ->(status) { where(status: status) }
79
+ # scope :created_after, ->(date) { where('created_at > ?', date) }
80
+
81
+ # Instance methods
82
+ # Add your custom methods here
83
+ # def full_name
84
+ # "#{first_name} #{last_name}"
85
+ # end
86
+
87
+ # Facets
88
+ json_facet :short, fields: [:id<%
89
+ # Include common identifying/summary fields for short facet
90
+ short_attributes = attributes.select do |attr|
91
+ # Include common identifying fields
92
+ identifying_fields = %w[name title label slug username email status state active published visible enabled]
93
+ # Include simple types but exclude large content and timestamps
94
+ simple_types = [:string, :integer, :boolean, :decimal, :float]
95
+ # Exclude large content fields, timestamps, and security-sensitive fields
96
+ excluded_patterns = /\A(description|content|body|notes|comment|bio|about|summary|created_at|updated_at|deleted_at|password|digest|token|secret|key|salt|encrypted|confirmation|unlock|reset|api_key|access_token|refresh_token)\z/i
97
+
98
+ # Always exclude if the field contains security-sensitive words
99
+ security_patterns = /(password|digest|token|secret|key|salt|encrypted|confirmation|unlock|reset|api_key)/i
100
+
101
+ identifying_fields.include?(attr.name.to_s) ||
102
+ (simple_types.include?(attr.type) &&
103
+ attr.name.to_s !~ excluded_patterns &&
104
+ attr.name.to_s !~ security_patterns &&
105
+ attr.name.to_s.length < 20)
106
+ end
107
+ short_attributes.each do |attribute| -%>, :<%= attribute.name %><% end %>]
108
+ json_facet :details, fields: [:id<%
109
+ # Include most fields except timestamps, security-sensitive, and internal Rails fields for details facet
110
+ detail_attributes = attributes.reject do |attr|
111
+ # Exclude timestamps and internal fields
112
+ excluded_patterns = /\A(created_at|updated_at|deleted_at|password_digest|reset_password_token|confirmation_token|unlock_token)\z/i
113
+ # Always exclude security-sensitive fields
114
+ security_patterns = /(password|digest|token|secret|key|salt|encrypted|confirmation|unlock|reset|api_key|access_token|refresh_token)/i
115
+ # Exclude binary and large data types
116
+ excluded_types = [:binary]
117
+
118
+ excluded_patterns.match?(attr.name.to_s) ||
119
+ security_patterns.match?(attr.name.to_s) ||
120
+ excluded_types.include?(attr.type)
121
+ end
122
+ detail_attributes.each do |attribute| -%>, :<%= attribute.name %><% end -%><% attributes.select { |attr| attr.type == :references }.each do |reference| -%>, :<%= reference.name %><% end -%><% unless options[:skip_tenancy] -%>, :organization, :agency<% end -%>]
123
+
124
+ # Example of more complex json_facet configurations:
125
+ #
126
+ # # Include associated models
127
+ # json_facet :author, include: [:user]
128
+ #
129
+ # # Include multiple associations
130
+ # json_facet :with_comments, include: [:user, :comments]
131
+ #
132
+ # # Include nested associations with custom names
133
+ # json_facet :detailed, include_as: { user: :author, comments: :replies }
134
+ #
135
+ # # Use methods for computed attributes
136
+ # json_facet :computed, methods: [:full_name, :calculated_total]
137
+ #
138
+ # # Combine fields, methods and includes
139
+ # json_facet :complete, :name, :email, methods: [:status], include: [:posts]
140
+
141
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ class <%= controller_class_name_with_namespace %>Controller < <%= api_controller_class_name %>
4
+ # Specify the Graphiti resource class for this controller
5
+ self.resource = <%= class_name %>Resource
6
+
7
+ # All CRUD methods (index, show, create, update, destroy) are inherited from <%= api_controller_class_name %>
8
+ # The inherited methods provide:
9
+ # - JSON:API compliant responses
10
+ # - Automatic filtering, sorting, and pagination
11
+ # - Resource-based parameter handling
12
+ # - Error handling
13
+ # - Authentication
14
+
15
+ # Callbacks can be added to run code before or after the default CRUD actions
16
+ # Use standard Rails before_action and after_action callbacks:
17
+ #
18
+ # before_action :your_method_name, only: [:create, :update]
19
+ # after_action :another_method, only: :destroy
20
+ #
21
+ # Example callbacks:
22
+ #
23
+ # before_action :set_defaults, only: :create
24
+ # before_action :check_ownership, only: [:update, :destroy]
25
+ # after_action :notify_users, only: [:create, :update, :destroy]
26
+ #
27
+ # def set_defaults
28
+ # # Set default values before creation
29
+ # end
30
+ #
31
+ # def check_ownership
32
+ # # Verify resource ownership
33
+ # end
34
+ #
35
+ # def notify_users
36
+ # # Send notifications after changes
37
+ # end
38
+
39
+ # Override any inherited methods here as a last resort if you need custom behavior that can't be handled by callbacks:
40
+ #
41
+ # def index
42
+ # # Custom index logic (e.g., additional filtering)
43
+ # resources = resource.all(params)
44
+ # respond_with(resources)
45
+ # end
46
+ #
47
+ # def create
48
+ # # Custom creation logic (e.g., set current user)
49
+ # resource_instance = resource.build(params)
50
+ # resource_instance.data.user = current_user # Example: set current user
51
+ #
52
+ # if resource_instance.save
53
+ # respond_with(resource_instance, status: :created)
54
+ # else
55
+ # respond_with(resource_instance.errors, status: :unprocessable_entity)
56
+ # end
57
+ # end
58
+ #
59
+ # def show
60
+ # # Custom show logic (e.g., check permissions)
61
+ # resource_instance = resource.find(params)
62
+ # authorize! :read, resource_instance.data # Example: authorization check
63
+ # respond_with(resource_instance)
64
+ # end
65
+
66
+ private
67
+
68
+ # Alternative: Override the resource method instead of using self.resource
69
+ # def resource
70
+ # <%= class_name %>Resource
71
+ # end
72
+
73
+ # Add any private helper methods specific to <%= class_name.downcase %> here:
74
+ #
75
+ # def authorize_<%= singular_table_name %>_access
76
+ # # Custom authorization logic
77
+ # end
78
+ #
79
+ # def additional_<%= singular_table_name %>_setup
80
+ # # Custom setup logic
81
+ # end
82
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ class <%= class_name %> < ApplicationRecord
4
+ # Graphiti uses resource classes for serialization, so no facets needed here
5
+ # Serialization is handled by the Graphiti resource class in app/resources/
6
+
7
+ # Validations
8
+ # Add your model validations here
9
+ # validates :name, presence: true
10
+ # validates :email, presence: true, uniqueness: true
11
+
12
+ # Associations
13
+ # Add your model associations here
14
+ # belongs_to :user
15
+ # has_many :comments, dependent: :destroy
16
+
17
+ # Scopes (useful for Graphiti resource filtering)
18
+ # scope :active, -> { where(active: true) }
19
+ # scope :by_status, ->(status) { where(status: status) }
20
+ # scope :created_after, ->(date) { where('created_at > ?', date) }
21
+
22
+ # Instance methods
23
+ # Add your custom methods here
24
+ # def full_name
25
+ # "#{first_name} #{last_name}"
26
+ # end
27
+
28
+ private
29
+
30
+ # Private methods
31
+ # Add any private helper methods here
32
+ end