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,433 @@
1
+ require_relative '../core/relationship_inferrer'
2
+ require_relative '../core/named_base'
3
+
4
+ module PropelApi
5
+ class ResourceGenerator < PropelApi::NamedBase
6
+ source_root File.expand_path("../templates", __dir__)
7
+
8
+ desc <<~DESC
9
+ Generate a complete API resource with model, migration, controller, and routes
10
+
11
+ ASSOCIATION EXAMPLES:
12
+
13
+ Standard belongs_to/has_many (automatic):
14
+ rails generate propel_api Article title:string user:references category:references
15
+ → Article belongs_to :user, :category
16
+ → User/Category has_many :articles, dependent: :destroy
17
+
18
+ Polymorphic associations:
19
+ rails generate propel_api Comment content:text commentable:references
20
+ → Comment belongs_to :commentable, polymorphic: true
21
+
22
+ rails generate propel_api Attachment name:string resource_parent:references
23
+ → Attachment belongs_to :resource, polymorphic: true
24
+
25
+
26
+
27
+ Skip associations entirely:
28
+ rails generate propel_api Article title:string user:references --skip-associations
29
+ → No associations generated (manual setup required)
30
+ DESC
31
+
32
+ argument :attributes, type: :array, default: [], banner: "field:type field:type"
33
+
34
+ class_option :skip_associations,
35
+ type: :boolean,
36
+ desc: "Skip automatic association generation"
37
+
38
+ class_option :skip_tenancy,
39
+ type: :boolean,
40
+ desc: "Skip multi-tenancy foundation (organization and agency)"
41
+
42
+ def create_migration
43
+ initialize_propel_api_settings
44
+
45
+ if behavior == :revoke
46
+ # Find the migration file
47
+ migration_files = Dir[File.join(destination_root, "db/migrate/*_create_#{table_name}.rb")]
48
+
49
+ if migration_files.any?
50
+ migration_file = migration_files.first
51
+ migration_version = File.basename(migration_file).split('_').first
52
+
53
+ # Check if migration was executed
54
+ if migration_was_executed?(migration_version)
55
+ say "\n" + "="*80, :red
56
+ say "⚠️ MIGRATION ALREADY EXECUTED!", :red
57
+ say "="*80, :red
58
+ say "\n📋 Migration #{migration_version} (create_#{table_name}) has been executed.", :yellow
59
+ say "\n🚨 Rails does not automatically rollback migrations for safety reasons.", :yellow
60
+ say "\n✅ To safely destroy this resource:", :green
61
+ say " 1. First rollback: rails db:rollback", :green
62
+ say " 2. Then destroy: rails destroy propel_api #{class_name}", :green
63
+ say "\n💡 Alternative approaches:", :cyan
64
+ say " • Check migration status: rails db:migrate:status"
65
+ say " • Rollback specific: rails db:migrate:down VERSION=#{migration_version}"
66
+ say "\n" + "="*80, :red
67
+ raise Thor::Error, "Please rollback the migration first, then re-run destroy command."
68
+ else
69
+ # Migration not executed, check if safe to delete
70
+ if safe_to_delete_migration?(migration_version)
71
+ say "Removing migration: #{File.basename(migration_file)}", :red
72
+ File.delete(migration_file)
73
+ else
74
+ say "\n" + "="*80, :yellow
75
+ say "⚠️ MIGRATION CLEANUP REQUIRED", :yellow
76
+ say "="*80, :yellow
77
+ say "\n📋 Found unexecuted migration: #{File.basename(migration_file)}", :cyan
78
+ say "\n🚨 Cannot auto-delete due to intervening migrations.", :yellow
79
+ say "💡 Please manually remove when safe:", :green
80
+ say " rm #{migration_file}"
81
+ say "\n" + "="*80, :yellow
82
+ end
83
+ end
84
+ else
85
+ say "No migration file found for #{table_name}", :yellow
86
+ end
87
+ else
88
+ generate :migration, "create_#{table_name}", *migration_attributes
89
+ # Post-process migration to make non-organization references nullable
90
+ update_migration_constraints
91
+ end
92
+ end
93
+
94
+ def create_model
95
+ initialize_propel_api_settings
96
+
97
+ # Initialize relationship inferrer for templates
98
+ @relationship_inferrer = RelationshipInferrer.new(class_name, attributes, {
99
+ skip_associations: options[:skip_associations]
100
+ })
101
+
102
+ # Validate attributes if not using --all_attributes and model already exists
103
+ if File.exist?(File.join(destination_root, "app/models/#{file_name}.rb"))
104
+ validate_attributes_exist
105
+ end
106
+
107
+ case @adapter
108
+ when 'propel_facets'
109
+ template "scaffold/facet_model_template.rb.tt", "app/models/#{file_name}.rb"
110
+ when 'graphiti'
111
+ template "scaffold/graphiti_model_template.rb.tt", "app/models/#{file_name}.rb"
112
+ else
113
+ raise "Unknown adapter: #{@adapter}. Run 'rails generate propel_api:install' first."
114
+ end
115
+ end
116
+
117
+ def create_controller
118
+ # Use shared method from Base class
119
+ create_propel_controller
120
+ end
121
+
122
+ def create_routes
123
+ # Use shared method from Base class
124
+ create_propel_routes
125
+ end
126
+
127
+ def create_tests
128
+ # Use shared method from Base class with all test types
129
+ create_propel_tests(test_types: [:model, :controller, :integration, :fixtures])
130
+ end
131
+
132
+ def create_seeds
133
+ if behavior == :revoke
134
+ # Remove seeds file when destroying
135
+ seeds_file_path = "db/seeds/#{table_name}_seeds.rb"
136
+ full_seeds_path = File.join(destination_root, seeds_file_path)
137
+ if File.exist?(full_seeds_path)
138
+ File.delete(full_seeds_path)
139
+ say "Removed seeds file: #{seeds_file_path}", :red
140
+ end
141
+
142
+ # Remove require from main seeds file
143
+ seeds_file = File.join(destination_root, "db/seeds.rb")
144
+ seeds_require = "require_relative 'seeds/#{table_name}_seeds'"
145
+
146
+ if File.exist?(seeds_file)
147
+ seeds_content = File.read(seeds_file)
148
+ if seeds_content.include?(seeds_require)
149
+ updated_content = seeds_content.dup
150
+
151
+ # Try multiple patterns to remove the require and any associated comment
152
+ patterns_to_try = [
153
+ # Pattern 1: Comment above the require
154
+ /\n# #{class_name} seeds\n#{Regexp.escape(seeds_require)}\n?/,
155
+ # Pattern 2: Comment with different casing
156
+ /\n# #{class_name.downcase} seeds\n#{Regexp.escape(seeds_require)}\n?/,
157
+ # Pattern 3: Just the require line with newlines
158
+ /\n#{Regexp.escape(seeds_require)}\n/,
159
+ # Pattern 4: Just the require line at start of line
160
+ /^#{Regexp.escape(seeds_require)}\n/,
161
+ # Pattern 5: Just the require line without trailing newline
162
+ /#{Regexp.escape(seeds_require)}/
163
+ ]
164
+
165
+ removed = false
166
+ patterns_to_try.each do |pattern|
167
+ if updated_content.match?(pattern)
168
+ updated_content = updated_content.gsub(pattern, '')
169
+ removed = true
170
+ break
171
+ end
172
+ end
173
+
174
+ if removed
175
+ # Clean up any double newlines that might result
176
+ updated_content = updated_content.gsub(/\n\n+/, "\n\n")
177
+ # Remove leading/trailing whitespace
178
+ updated_content = updated_content.strip + "\n" if updated_content.strip.present?
179
+
180
+ File.write(seeds_file, updated_content)
181
+ say "Removed require statement from db/seeds.rb", :red
182
+ else
183
+ say "Could not automatically remove require statement from db/seeds.rb", :yellow
184
+ end
185
+ end
186
+ end
187
+ else
188
+ initialize_propel_api_settings
189
+
190
+ # Generate seeds file
191
+ template "seeds/seeds_template.rb.tt", "db/seeds/#{table_name}_seeds.rb"
192
+
193
+ # Add require to main seeds file
194
+ seeds_file = File.join(destination_root, "db/seeds.rb")
195
+ seeds_require = "require_relative 'seeds/#{table_name}_seeds'"
196
+
197
+ if File.exist?(seeds_file)
198
+ seeds_content = File.read(seeds_file)
199
+ unless seeds_content.include?(seeds_require)
200
+ append_to_file "db/seeds.rb", "\n# #{class_name} seeds\n#{seeds_require}\n"
201
+ end
202
+ else
203
+ create_file "db/seeds.rb", "#{seeds_require}\n"
204
+ end
205
+ end
206
+ end
207
+
208
+ def show_completion_message
209
+ # Use shared completion message framework from Base class
210
+ generated_files = [
211
+ "📄 Model: app/models/#{file_name}.rb",
212
+ "🎮 Controller: app/controllers/#{controller_path}/#{controller_file_name}.rb",
213
+ "🗄️ Migration: db/migrate/*_create_#{table_name}.rb",
214
+ "🌱 Seeds: db/seeds/#{table_name}_seeds.rb",
215
+ "🧪 Model Tests: test/models/#{file_name}_test.rb",
216
+ "🧪 Controller Tests: test/controllers/#{controller_path}/#{controller_file_name}_test.rb",
217
+ "🧪 Integration Tests: test/integration/#{file_name}_api_test.rb",
218
+ "📋 Test Fixtures: test/fixtures/#{table_name}.yml"
219
+ ]
220
+
221
+ # Add Graphiti resource if using graphiti adapter
222
+ if @adapter == 'graphiti'
223
+ generated_files << "📊 Graphiti Resource: app/resources/#{file_name}_resource.rb"
224
+ end
225
+
226
+ next_steps = [
227
+ "Run migration: rails db:migrate",
228
+ "Generate test data: rails db:seed",
229
+ "Run tests: rails test test/models/#{file_name}_test.rb",
230
+ "Run all tests: rails test",
231
+ "Test your API: GET #{api_route_path}"
232
+ ]
233
+
234
+ if @adapter == 'propel_facets'
235
+ next_steps << "Customize facets in: app/models/#{file_name}.rb"
236
+ end
237
+
238
+ show_propel_completion_message(
239
+ resource_type: "Resource",
240
+ generated_files: generated_files,
241
+ next_steps: next_steps
242
+ )
243
+ end
244
+
245
+ private
246
+
247
+ def relationship_inferrer
248
+ @relationship_inferrer
249
+ end
250
+
251
+ # Override Base class method to enable Graphiti resource generation for main generator
252
+ def should_generate_graphiti_resource?
253
+ true
254
+ end
255
+
256
+ def route_name
257
+ file_name.pluralize
258
+ end
259
+
260
+ def migration_attributes
261
+ # Always include organization (required) and agency (optional) for multi-tenancy
262
+ # unless --skip-tenancy flag is used
263
+ if options[:skip_tenancy]
264
+ # Only include what the developer explicitly specified
265
+ attributes.map { |attr| "#{attr.name}:#{attr.type}" }
266
+ else
267
+ # Include multi-tenancy foundation by default
268
+ standard_attributes = ["organization:references", "agency:references"]
269
+
270
+ # Add user-specified attributes, but skip organization and agency if they're already specified
271
+ user_attributes = attributes.map { |attr| "#{attr.name}:#{attr.type}" }
272
+ user_attributes.reject! { |attr| attr.start_with?("organization:") || attr.start_with?("agency:") }
273
+
274
+ # Combine standard and user attributes
275
+ standard_attributes + user_attributes
276
+ end
277
+ end
278
+
279
+ def permitted_param_names
280
+ if options[:skip_tenancy]
281
+ # Only include what the developer explicitly specified
282
+ attributes.map do |attr|
283
+ # Convert references to foreign key names for permitted params
284
+ # e.g., organization:references becomes organization_id
285
+ if attr.type == :references
286
+ "#{attr.name}_id"
287
+ else
288
+ attr.name
289
+ end
290
+ end
291
+ else
292
+ # Always include organization_id (required) and agency_id (optional) for multi-tenancy
293
+ standard_params = ["organization_id", "agency_id"]
294
+
295
+ # Add user-specified attributes, but skip organization and agency if they're already specified
296
+ user_params = attributes.map do |attr|
297
+ # Convert references to foreign key names for permitted params
298
+ # e.g., organization:references becomes organization_id
299
+ if attr.type == :references
300
+ "#{attr.name}_id"
301
+ else
302
+ attr.name
303
+ end
304
+ end
305
+ user_params.reject! { |param| param == "organization_id" || param == "agency_id" }
306
+
307
+ # Combine standard and user params
308
+ standard_params + user_params
309
+ end
310
+ end
311
+
312
+ def namespaced?
313
+ false
314
+ end
315
+
316
+ def module_namespacing(&block)
317
+ yield
318
+ end
319
+
320
+ def api_namespaced?
321
+ @api_namespace.present? || @api_version.present?
322
+ end
323
+
324
+ def route_url
325
+ api_route_path
326
+ end
327
+
328
+ def orm_class
329
+ @orm_class ||= begin
330
+ # Simple ORM class mock for template compatibility
331
+ Class.new do
332
+ def self.all(class_name)
333
+ class_name.constantize.all
334
+ end
335
+
336
+ def self.build(class_name, params_method)
337
+ "#{class_name}.new(#{params_method})"
338
+ end
339
+
340
+ def self.find(class_name, finder)
341
+ "#{class_name}.find(#{finder})"
342
+ end
343
+ end.new
344
+ end
345
+ end
346
+
347
+ def migration_was_executed?(migration_version)
348
+ return false unless defined?(ActiveRecord::Base)
349
+
350
+ begin
351
+ # Ensure database connection exists
352
+ ActiveRecord::Base.connection
353
+
354
+ # Check if schema_migrations table exists (might not in fresh apps)
355
+ return false unless ActiveRecord::Base.connection.table_exists?('schema_migrations')
356
+
357
+ # Query for the specific migration version
358
+ result = ActiveRecord::Base.connection.execute(
359
+ "SELECT version FROM schema_migrations WHERE version = '#{migration_version}'"
360
+ )
361
+
362
+ # Check if we found the migration (different adapters return different result types)
363
+ case result
364
+ when Array
365
+ result.any?
366
+ else
367
+ result.to_a.any?
368
+ end
369
+
370
+ rescue => e
371
+ # If any error occurs (database not connected, etc.), assume safe to delete
372
+ say "Warning: Could not check migration status (#{e.message})", :yellow
373
+ false
374
+ end
375
+ end
376
+
377
+ def migration_timestamp
378
+ Time.current.utc.strftime("%Y%m%d%H%M%S")
379
+ end
380
+
381
+ def update_migration_constraints
382
+ # Find the most recent migration file for this table
383
+ migration_files = Dir[File.join(destination_root, "db/migrate/*_create_#{table_name}.rb")]
384
+ return unless migration_files.any?
385
+
386
+ migration_file = migration_files.sort.last
387
+ content = File.read(migration_file)
388
+
389
+ # Update references to be nullable except for organization
390
+ # organization:references should remain: null: false, foreign_key: true
391
+ # All other references should become: null: true, foreign_key: true
392
+ updated_content = content.gsub(/t\.references :(\w+), null: false, foreign_key: true/) do |match|
393
+ reference_name = $1
394
+ if reference_name == 'organization'
395
+ match # Keep organization as required (null: false)
396
+ else
397
+ "t.references :#{reference_name}, null: true, foreign_key: true"
398
+ end
399
+ end
400
+
401
+ # Write the updated migration back to file
402
+ File.write(migration_file, updated_content)
403
+ end
404
+
405
+ def safe_to_delete_migration?(migration_version)
406
+ # Get all migration files in the migrate directory
407
+ all_migrations = Dir[File.join(destination_root, "db/migrate/*.rb")]
408
+
409
+ # Extract just the timestamp versions from all migrations
410
+ all_versions = all_migrations.map do |file|
411
+ File.basename(file).split('_').first
412
+ end
413
+
414
+ # Check if any migrations have timestamps newer than our migration
415
+ newer_migrations = all_versions.select { |version| version > migration_version }
416
+
417
+ if newer_migrations.any?
418
+ # Check if any of the newer migrations have been executed
419
+ newer_executed = newer_migrations.any? { |version| migration_was_executed?(version) }
420
+
421
+ if newer_executed
422
+ # There are newer migrations that have been executed - unsafe to delete
423
+ return false
424
+ end
425
+ end
426
+
427
+ # Safe to delete if:
428
+ # 1. No newer migrations exist, OR
429
+ # 2. Newer migrations exist but none have been executed
430
+ true
431
+ end
432
+ end
433
+ end
@@ -0,0 +1,149 @@
1
+ # PropelApi Configuration
2
+ # This file was generated by: rails generate propel_api:install
3
+ #
4
+ # These settings control the default behavior for PropelApi generators.
5
+ # You can override these defaults when running generators with command line options.
6
+
7
+ module PropelApi
8
+ class Configuration
9
+ attr_accessor :adapter, :namespace, :version
10
+ attr_reader :attribute_filter
11
+
12
+ def initialize
13
+ # Default configuration values
14
+ @adapter = 'propel_facets' # Default serialization adapter
15
+ @namespace = 'api' # Default API namespace
16
+ @version = 'v1' # Default API version
17
+
18
+ # Initialize the configurable attribute filter
19
+ @attribute_filter = PropelApi::AttributeFilter.new
20
+ end
21
+ end
22
+
23
+ # Centralized, configurable attribute filtering service
24
+ # This is used by both templates and introspection to ensure consistency
25
+ class AttributeFilter
26
+ attr_accessor :sensitive_patterns, :excluded_patterns, :large_content_patterns
27
+
28
+ def initialize
29
+ # Security-sensitive field patterns
30
+ @sensitive_patterns = [
31
+ /(password|digest|token|secret|key|salt|encrypted|confirmation|unlock|reset|api_key|access_token|refresh_token)/i,
32
+ /\A(ssn|social_security|credit_card|cvv|pin|tax_id)\z/i
33
+ ]
34
+
35
+ # System/internal field patterns
36
+ @excluded_patterns = [
37
+ /\A(id|created_at|updated_at|deleted_at)\z/i,
38
+ /\A(password_digest|reset_password_token|confirmation_token|unlock_token|remember_token)\z/i,
39
+ /\A.*(_blob|_data|_binary)$/i
40
+ ]
41
+
42
+ # Large content field patterns (usually not suitable for permitted params)
43
+ @large_content_patterns = [
44
+ /\A(description|content|body|notes|comment|bio|about|summary|text|html|markdown)\z/i
45
+ ]
46
+ end
47
+
48
+ # Check if an attribute should be excluded from permitted params
49
+ def exclude_from_permitted_params?(attribute_name, attribute_type = nil)
50
+ attr_str = attribute_name.to_s
51
+
52
+ # Check all pattern categories
53
+ sensitive_patterns.any? { |pattern| pattern.match?(attr_str) } ||
54
+ excluded_patterns.any? { |pattern| pattern.match?(attr_str) } ||
55
+ large_content_patterns.any? { |pattern| pattern.match?(attr_str) } ||
56
+ (attribute_type == :binary)
57
+ end
58
+
59
+ # Check if an attribute should be included in short facets (for templates)
60
+ def include_in_short_facet?(attribute_name, attribute_type)
61
+ return false if exclude_from_permitted_params?(attribute_name, attribute_type)
62
+
63
+ attr_str = attribute_name.to_s
64
+
65
+ # Include common identifying fields
66
+ identifying_fields = %w[name title label slug username email status state active published visible enabled]
67
+ simple_types = [:string, :integer, :boolean, :decimal, :float]
68
+
69
+ # Must be identifying field OR (simple type AND short name)
70
+ identifying_fields.include?(attr_str) ||
71
+ (simple_types.include?(attribute_type) && attr_str.length < 20)
72
+ end
73
+
74
+ # Check if an attribute should be included in details facets (for templates)
75
+ def include_in_details_facet?(attribute_name, attribute_type)
76
+ !exclude_from_permitted_params?(attribute_name, attribute_type)
77
+ end
78
+
79
+ # Helper methods for easy customization
80
+ def add_sensitive_pattern(pattern)
81
+ @sensitive_patterns << pattern
82
+ end
83
+
84
+ def add_excluded_pattern(pattern)
85
+ @excluded_patterns << pattern
86
+ end
87
+
88
+ def add_large_content_pattern(pattern)
89
+ @large_content_patterns << pattern
90
+ end
91
+
92
+ # Get a summary of what's being filtered (for debugging/documentation)
93
+ def filtering_summary
94
+ {
95
+ sensitive_patterns: @sensitive_patterns.map(&:source),
96
+ excluded_patterns: @excluded_patterns.map(&:source),
97
+ large_content_patterns: @large_content_patterns.map(&:source)
98
+ }
99
+ end
100
+ end
101
+
102
+ # Class methods for configuration access
103
+ class << self
104
+ def configuration
105
+ @configuration ||= Configuration.new
106
+ end
107
+
108
+ def configure
109
+ yield(configuration)
110
+ end
111
+
112
+ def reset_configuration!
113
+ @configuration = Configuration.new
114
+ end
115
+
116
+ # Convenience method for accessing the filter
117
+ def attribute_filter
118
+ configuration.attribute_filter
119
+ end
120
+ end
121
+ end
122
+
123
+ # Configure PropelApi with the options selected during installation
124
+ PropelApi.configure do |config|
125
+ # Serialization adapter: 'propel_facets' or 'graphiti'
126
+ config.adapter = '<%= @adapter %>'
127
+
128
+ # API namespace: 'api', 'admin_api', etc. or nil for no namespace
129
+ config.namespace = <%= @api_namespace ? "'#{@api_namespace}'" : 'nil' %>
130
+
131
+ # API version: 'v1', 'v2', etc. or nil for no versioning
132
+ config.version = <%= @api_version ? "'#{@api_version}'" : 'nil' %>
133
+
134
+ # Customize attribute filtering for your project's naming conventions
135
+ # Examples:
136
+ #
137
+ # Add custom sensitive patterns:
138
+ # config.attribute_filter.add_sensitive_pattern(/private_.*/)
139
+ # config.attribute_filter.add_sensitive_pattern(/\A.*_secret\z/i)
140
+ #
141
+ # Add custom excluded patterns:
142
+ # config.attribute_filter.add_excluded_pattern(/\A(audit_|legacy_).*/)
143
+ #
144
+ # Add custom large content patterns:
145
+ # config.attribute_filter.add_large_content_pattern(/\A.*_(text|html|json)\z/i)
146
+ #
147
+ # View current filtering rules:
148
+ # puts PropelApi.attribute_filter.filtering_summary
149
+ end
@@ -0,0 +1,79 @@
1
+ class <%= api_controller_class_name %> < ApplicationController
2
+ include Graphiti::Rails
3
+ include Graphiti::Responders
4
+ include PropelAuthentication
5
+
6
+ before_action :authenticate_user
7
+
8
+ # Graphiti handles CRUD operations through resources automatically
9
+ # Individual controllers should define their resource class like:
10
+ # class UsersController < <%= api_controller_class_name %>
11
+ # self.resource = UserResource
12
+ # end
13
+ #
14
+ # Resources should be created using:
15
+ # rails generate graphiti:resource User
16
+
17
+ def index
18
+ resources = resource.all(params)
19
+ respond_with(resources)
20
+ end
21
+
22
+ def show
23
+ resource_instance = resource.find(params)
24
+ respond_with(resource_instance)
25
+ end
26
+
27
+ def create
28
+ resource_instance = resource.build(params)
29
+
30
+ if resource_instance.save
31
+ respond_with(resource_instance, status: :created)
32
+ else
33
+ respond_with(resource_instance.errors, status: :unprocessable_entity)
34
+ end
35
+ end
36
+
37
+ def update
38
+ resource_instance = resource.find(params)
39
+
40
+ if resource_instance.update_attributes
41
+ respond_with(resource_instance)
42
+ else
43
+ respond_with(resource_instance.errors, status: :unprocessable_entity)
44
+ end
45
+ end
46
+
47
+ def destroy
48
+ resource_instance = resource.find(params)
49
+ resource_instance.destroy
50
+
51
+ respond_with(resource_instance)
52
+ end
53
+
54
+ private
55
+
56
+ # Override in subclasses to specify the Graphiti resource class
57
+ # Example:
58
+ # def resource
59
+ # UserResource
60
+ # end
61
+ def resource
62
+ @resource ||= infer_resource_class.new
63
+ end
64
+
65
+ # Auto-infer resource class from controller name
66
+ # e.g., UsersController -> UserResource
67
+ def infer_resource_class
68
+ resource_class_name = "#{controller_name.classify}Resource"
69
+ resource_class_name.constantize
70
+
71
+ raise "#{resource_class_name} not found. Please create it using: rails generate graphiti:resource #{controller_name.classify}"
72
+ end
73
+
74
+ # Graphiti handles parameter filtering through resources automatically
75
+ # No need for manual strong parameters - resources define allowed attributes
76
+ # Define attributes in your resource files:
77
+ # attribute :name, :string
78
+ # attribute :email, :string
79
+ end