propel_api 0.2.1 → 0.3.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 (25) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +97 -0
  3. data/README.md +239 -4
  4. data/lib/generators/propel_api/controller/controller_generator.rb +26 -0
  5. data/lib/generators/propel_api/core/named_base.rb +97 -1
  6. data/lib/generators/propel_api/core/relationship_inferrer.rb +4 -11
  7. data/lib/generators/propel_api/install/install_generator.rb +39 -2
  8. data/lib/generators/propel_api/resource/resource_generator.rb +205 -63
  9. data/lib/generators/propel_api/templates/config/propel_api.rb.tt +26 -1
  10. data/lib/generators/propel_api/templates/controllers/api_base_controller.rb +75 -0
  11. data/lib/generators/propel_api/templates/controllers/api_controller_graphiti.rb +3 -3
  12. data/lib/generators/propel_api/templates/controllers/api_controller_propel_facets.rb +22 -12
  13. data/lib/generators/propel_api/templates/controllers/example_controller.rb.tt +2 -2
  14. data/lib/generators/propel_api/templates/errors/propel_api_csrf_error.rb +19 -0
  15. data/lib/generators/propel_api/templates/scaffold/facet_controller_template.rb.tt +41 -8
  16. data/lib/generators/propel_api/templates/scaffold/facet_model_template.rb.tt +43 -10
  17. data/lib/generators/propel_api/templates/scaffold/graphiti_controller_template.rb.tt +1 -1
  18. data/lib/generators/propel_api/templates/scaffold/graphiti_resource_template.rb.tt +2 -2
  19. data/lib/generators/propel_api/templates/seeds/seeds_template.rb.tt +65 -17
  20. data/lib/generators/propel_api/templates/tests/controller_test_template.rb.tt +40 -6
  21. data/lib/generators/propel_api/templates/tests/fixtures_template.yml.tt +61 -18
  22. data/lib/generators/propel_api/templates/tests/integration_test_template.rb.tt +154 -42
  23. data/lib/generators/propel_api/templates/tests/model_test_template.rb.tt +20 -0
  24. data/lib/propel_api.rb +1 -1
  25. metadata +24 -2
@@ -1,8 +1,29 @@
1
1
  # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
2
2
 
3
3
  one:
4
+ <% # Handle polymorphic associations first -%>
5
+ <% polymorphic_associations.each do |poly_assoc| -%>
6
+ <% parent_info = polymorphic_parent_for_fixture(poly_assoc[:field_name], 0) -%>
7
+ <%= poly_assoc[:field_name] %>: <%= parent_info[:fixture_name] %> (<%= parent_info[:parent_type] %>)
8
+ <% end -%>
9
+ <% # Handle regular references (excluding polymorphic ones) -%>
10
+ <% polymorphic_field_names = polymorphic_associations.map { |assoc| assoc[:field_name].to_s } -%>
4
11
  <% attributes.each do |attribute| -%>
5
- <% if attribute.type == :references -%>
12
+ <% # Skip polymorphic references - check both Rails detection and our field names -%>
13
+ <% is_polymorphic = (attribute.respond_to?(:polymorphic?) && attribute.polymorphic?) || polymorphic_field_names.include?(attribute.name.to_s) -%>
14
+ <% if attribute.type == :references && !is_polymorphic -%>
15
+ <% if attribute.name == 'organization' -%>
16
+ <%= attribute.name %>: acme_org
17
+ <% elsif attribute.name == 'user' -%>
18
+ <%= attribute.name %>: john_user
19
+ <% elsif attribute.name == 'agency' -%>
20
+ <%= attribute.name %>: marketing_agency
21
+ <% else -%>
22
+ <%= attribute.name %>: one
23
+ <% end -%>
24
+ <% elsif attribute.type == :references && is_polymorphic -%>
25
+ <% # Polymorphic references are handled above, skip them here -%>
26
+ <% elsif attribute.type == :string -%>
6
27
  <% if attribute.name == 'organization' -%>
7
28
  <%= attribute.name %>: acme_org
8
29
  <% elsif attribute.name == 'user' -%>
@@ -19,7 +40,7 @@ one:
19
40
  <%= attribute.name %>: test_user_1
20
41
  <% elsif attribute.name.to_s.match?(/\A(phone|phone_number)\z/i) -%>
21
42
  <%= attribute.name %>: "+1-555-0001"
22
- <% elsif attribute.name.to_s.match?(/\A(url|website|web_address)\z/i) -%>
43
+ <% elsif attribute.name.to_s.match?(/\A(url|website|web_address|domain|domain_name)\z/i) -%>
23
44
  <%= attribute.name %>: "https://example1.com"
24
45
  <% elsif attribute.name.to_s.match?(/\A(name|title|label)\z/i) -%>
25
46
  <%= attribute.name %>: "Test <%= attribute.name.humanize %> One"
@@ -80,11 +101,11 @@ one:
80
101
  <%= attribute.name %>: "2024-06-15 10:30:00"
81
102
  <% elsif attribute.type == :time -%>
82
103
  <%= attribute.name %>: "10:30:00"
83
- <% elsif attribute.type == :json || attribute.name.to_s.match?(/^(meta|settings)$/) -%>
84
- <% if attribute.name == 'meta' -%>
85
- <%= attribute.name %>: { fixture_meta: "test_one", environment: "test", record_type: "<%= singular_table_name %>" }
104
+ <% elsif attribute.type == :json || attribute.name.to_s.match?(/^(metadata|settings)$/) -%>
105
+ <% if attribute.name == 'metadata' -%>
106
+ <%= attribute.name %>: { resource_type: "<%= singular_table_name %>", category: "test", tags: ["fixture", "one"], attributes: { priority: "high", status_info: { verified: true, active: true } } }
86
107
  <% elsif attribute.name == 'settings' -%>
87
- <%= attribute.name %>: { dark_mode: false, money_format: "usd", language: "en", test_mode: true }
108
+ <%= attribute.name %>: { ui_preferences: { theme: "light", language: "en", notifications: { email: true, sms: false, push: true } }, feature_flags: { beta_features: false, analytics: true } }
88
109
  <% else -%>
89
110
  <%= attribute.name %>: { test_data: "fixture_one" }
90
111
  <% end -%>
@@ -94,8 +115,17 @@ one:
94
115
  <% end -%>
95
116
 
96
117
  two:
118
+ <% # Handle polymorphic associations first -%>
119
+ <% polymorphic_associations.each do |poly_assoc| -%>
120
+ <% parent_info = polymorphic_parent_for_fixture(poly_assoc[:field_name], 1) -%>
121
+ <%= poly_assoc[:field_name] %>: <%= parent_info[:fixture_name] %> (<%= parent_info[:parent_type] %>)
122
+ <% end -%>
123
+ <% # Handle regular references (excluding polymorphic ones) -%>
124
+ <% polymorphic_field_names = polymorphic_associations.map { |assoc| assoc[:field_name].to_s } -%>
97
125
  <% attributes.each do |attribute| -%>
98
- <% if attribute.type == :references -%>
126
+ <% # Skip polymorphic references - check both Rails detection and our field names -%>
127
+ <% is_polymorphic = (attribute.respond_to?(:polymorphic?) && attribute.polymorphic?) || polymorphic_field_names.include?(attribute.name.to_s) -%>
128
+ <% if attribute.type == :references && !is_polymorphic -%>
99
129
  <% if attribute.name == 'organization' -%>
100
130
  <%= attribute.name %>: tech_startup
101
131
  <% elsif attribute.name == 'user' -%>
@@ -105,6 +135,8 @@ two:
105
135
  <% else -%>
106
136
  <%= attribute.name %>: one
107
137
  <% end -%>
138
+ <% elsif attribute.type == :references && is_polymorphic -%>
139
+ <% # Polymorphic references are handled above, skip them here -%>
108
140
  <% elsif attribute.type == :string -%>
109
141
  <% if attribute.name.to_s.match?(/\A(email|email_address)\z/i) -%>
110
142
  <%= attribute.name %>: test2@example.com
@@ -112,7 +144,7 @@ two:
112
144
  <%= attribute.name %>: test_user_2
113
145
  <% elsif attribute.name.to_s.match?(/\A(phone|phone_number)\z/i) -%>
114
146
  <%= attribute.name %>: "+1-555-0002"
115
- <% elsif attribute.name.to_s.match?(/\A(url|website|web_address)\z/i) -%>
147
+ <% elsif attribute.name.to_s.match?(/\A(url|website|web_address|domain|domain_name)\z/i) -%>
116
148
  <%= attribute.name %>: "https://example2.com"
117
149
  <% elsif attribute.name.to_s.match?(/\A(name|title|label)\z/i) -%>
118
150
  <%= attribute.name %>: "Test <%= attribute.name.humanize %> Two"
@@ -173,11 +205,11 @@ two:
173
205
  <%= attribute.name %>: "2023-12-25 15:45:00"
174
206
  <% elsif attribute.type == :time -%>
175
207
  <%= attribute.name %>: "15:45:00"
176
- <% elsif attribute.type == :json || attribute.name.to_s.match?(/^(meta|settings)$/) -%>
177
- <% if attribute.name == 'meta' -%>
178
- <%= attribute.name %>: { fixture_meta: "test_two", environment: "test", record_type: "<%= singular_table_name %>" }
208
+ <% elsif attribute.type == :json || attribute.name.to_s.match?(/^(metadata|settings)$/) -%>
209
+ <% if attribute.name == 'metadata' -%>
210
+ <%= attribute.name %>: { resource_type: "<%= singular_table_name %>", category: "premium", tags: ["fixture", "two"], attributes: { priority: "medium", status_info: { verified: false, active: true } } }
179
211
  <% elsif attribute.name == 'settings' -%>
180
- <%= attribute.name %>: { dark_mode: true, money_format: "eur", language: "fr", test_mode: true }
212
+ <%= attribute.name %>: { ui_preferences: { theme: "dark", language: "fr", notifications: { email: true, sms: true, push: false } }, feature_flags: { beta_features: true, analytics: false } }
181
213
  <% else -%>
182
214
  <%= attribute.name %>: { test_data: "fixture_two" }
183
215
  <% end -%>
@@ -187,8 +219,17 @@ two:
187
219
  <% end -%>
188
220
 
189
221
  three:
222
+ <% # Handle polymorphic associations first -%>
223
+ <% polymorphic_associations.each do |poly_assoc| -%>
224
+ <% parent_info = polymorphic_parent_for_fixture(poly_assoc[:field_name], 2) -%>
225
+ <%= poly_assoc[:field_name] %>: <%= parent_info[:fixture_name] %> (<%= parent_info[:parent_type] %>)
226
+ <% end -%>
227
+ <% # Handle regular references (excluding polymorphic ones) -%>
228
+ <% polymorphic_field_names = polymorphic_associations.map { |assoc| assoc[:field_name].to_s } -%>
190
229
  <% attributes.each do |attribute| -%>
191
- <% if attribute.type == :references -%>
230
+ <% # Skip polymorphic references - check both Rails detection and our field names -%>
231
+ <% is_polymorphic = (attribute.respond_to?(:polymorphic?) && attribute.polymorphic?) || polymorphic_field_names.include?(attribute.name.to_s) -%>
232
+ <% if attribute.type == :references && !is_polymorphic -%>
192
233
  <% if attribute.name == 'organization' -%>
193
234
  <%= attribute.name %>: acme_org
194
235
  <% elsif attribute.name == 'user' -%>
@@ -198,6 +239,8 @@ three:
198
239
  <% else -%>
199
240
  <%= attribute.name %>: one
200
241
  <% end -%>
242
+ <% elsif attribute.type == :references && is_polymorphic -%>
243
+ <% # Polymorphic references are handled above, skip them here -%>
201
244
  <% elsif attribute.type == :string -%>
202
245
  <% if attribute.name.to_s.match?(/\A(email|email_address)\z/i) -%>
203
246
  <%= attribute.name %>: test3@example.com
@@ -205,7 +248,7 @@ three:
205
248
  <%= attribute.name %>: test_user_3
206
249
  <% elsif attribute.name.to_s.match?(/\A(phone|phone_number)\z/i) -%>
207
250
  <%= attribute.name %>: "+1-555-0003"
208
- <% elsif attribute.name.to_s.match?(/\A(url|website|web_address)\z/i) -%>
251
+ <% elsif attribute.name.to_s.match?(/\A(url|website|web_address|domain|domain_name)\z/i) -%>
209
252
  <%= attribute.name %>: "https://example3.com"
210
253
  <% elsif attribute.name.to_s.match?(/\A(name|title|label)\z/i) -%>
211
254
  <%= attribute.name %>: "Test <%= attribute.name.humanize %> Three"
@@ -266,11 +309,11 @@ three:
266
309
  <%= attribute.name %>: "2025-03-10 08:15:00"
267
310
  <% elsif attribute.type == :time -%>
268
311
  <%= attribute.name %>: "08:15:00"
269
- <% elsif attribute.type == :json || attribute.name.to_s.match?(/^(meta|settings)$/) -%>
270
- <% if attribute.name == 'meta' -%>
271
- <%= attribute.name %>: { fixture_meta: "test_three", environment: "test", record_type: "<%= singular_table_name %>" }
312
+ <% elsif attribute.type == :json || attribute.name.to_s.match?(/^(metadata|settings)$/) -%>
313
+ <% if attribute.name == 'metadata' -%>
314
+ <%= attribute.name %>: { resource_type: "<%= singular_table_name %>", category: "basic", tags: ["fixture", "three"], attributes: { priority: "low", status_info: { verified: true, active: false } } }
272
315
  <% elsif attribute.name == 'settings' -%>
273
- <%= attribute.name %>: { dark_mode: true, money_format: "gbp", language: "es", test_mode: true }
316
+ <%= attribute.name %>: { ui_preferences: { theme: "auto", language: "es", notifications: { email: false, sms: true, push: true } }, feature_flags: { beta_features: false, analytics: true } }
274
317
  <% else -%>
275
318
  <%= attribute.name %>: { test_data: "fixture_three" }
276
319
  <% end -%>
@@ -2,12 +2,28 @@
2
2
 
3
3
  require "test_helper"
4
4
 
5
+ <%
6
+ # Check attributes directly instead of database columns (more reliable during generation)
7
+ has_agency_id = attributes.any? { |attr| attr.name == 'agency' && attr.type == :references }
8
+ has_user_id = attributes.any? { |attr| attr.name == 'user' && attr.type == :references }
9
+ -%>
10
+
5
11
  class <%= class_name %>ApiTest < ActionDispatch::IntegrationTest
6
12
 
7
13
  def setup
8
14
  @organization = organizations(:acme_org)
9
15
  @user = users(:john_user)
10
16
  @agency = agencies(:marketing_agency)
17
+ <% polymorphic_associations.each do |assoc| -%>
18
+ <% assoc[:parent_types].each_with_index do |parent_type, index| -%>
19
+ <% if index == 0 -%>
20
+ @<%= parent_type.underscore %> = <%= parent_type.underscore.pluralize %>(<%= case parent_type.underscore; when 'agency'; ':marketing_agency'; when 'user'; ':john_user'; when 'organization'; ':acme_org'; else; ':one'; end %>)
21
+ @<%= assoc[:field_name] %> = @<%= parent_type.underscore %> # Default polymorphic parent for tests
22
+ <% else -%>
23
+ @<%= parent_type.underscore %> = <%= parent_type.underscore.pluralize %>(<%= case parent_type.underscore; when 'agency'; ':marketing_agency'; when 'user'; ':john_user'; when 'organization'; ':acme_org'; else; ':one'; end %>)
24
+ <% end -%>
25
+ <% end -%>
26
+ <% end -%>
11
27
  <% if singular_table_name == 'organization' -%>
12
28
  @<%= singular_table_name %> = <%= table_name %>(:acme_org)
13
29
  <% elsif singular_table_name == 'user' -%>
@@ -16,12 +32,27 @@ class <%= class_name %>ApiTest < ActionDispatch::IntegrationTest
16
32
  @<%= singular_table_name %> = <%= table_name %>(:marketing_agency)
17
33
  <% else -%>
18
34
  @<%= singular_table_name %> = <%= table_name %>(:one)
35
+ <% end -%>
36
+ <% if has_user_reference? -%>
37
+ @user = users(:john_user)
38
+ <% end -%>
39
+ <% # Set up polymorphic associations using --parents specification -%>
40
+ <% polymorphic_associations.each do |assoc| -%>
41
+ <% if assoc[:parent_types] && assoc[:parent_types].any? -%>
42
+ <% first_parent = assoc[:parent_types].first -%>
43
+ # Set up polymorphic association for <%= assoc[:field_name] %> using specified parents
44
+ @<%= first_parent.underscore %> = <%= first_parent.underscore.pluralize %>(<%= case first_parent.underscore; when 'agency'; ':marketing_agency'; when 'user'; ':john_user'; when 'organization'; ':acme_org'; else; ':one'; end %>)
45
+ @<%= assoc[:field_name] %> = @<%= first_parent.underscore %> # Use first specified parent type
46
+ <% assoc[:parent_types][1..-1].each do |parent_type| -%>
47
+ @<%= parent_type.underscore %> = <%= parent_type.underscore.pluralize %>(<%= case parent_type.underscore; when 'agency'; ':marketing_agency'; when 'user'; ':john_user'; when 'organization'; ':acme_org'; else; ':one'; end %>)
48
+ <% end -%>
49
+ <% end -%>
19
50
  <% end -%>
20
51
  @token = @user.generate_jwt_token
21
52
  @auth_headers = { 'Authorization' => "Bearer #{@token}" }
22
53
 
23
54
  # Ensure test <%= singular_table_name %> belongs to test user's organization
24
- <% unless singular_table_name == 'organization' -%>
55
+ <% if has_organization_reference? -%>
25
56
  @<%= singular_table_name %>.update!(organization: @organization)
26
57
  <% end -%>
27
58
  end
@@ -45,12 +76,21 @@ class <%= class_name %>ApiTest < ActionDispatch::IntegrationTest
45
76
 
46
77
  <% attributes.each_with_index do |attribute, index| -%>
47
78
  <% if attribute.type == :references -%>
79
+ <% if attribute.respond_to?(:polymorphic?) && attribute.polymorphic? -%>
80
+ <%= attribute.name %>_id: @<%= attribute.name %>.id,
81
+ <%= attribute.name %>_type: @<%= attribute.name %>.class.name<%= ',' if index < attributes.length - 1 %>
82
+ <% else -%>
48
83
  <%= attribute.name %>_id: @<%= attribute.name %>.id<%= ',' if index < attributes.length - 1 %>
84
+ <% end -%>
49
85
  <% elsif attribute.type == :string && !reference_names.include?(attribute.name) -%>
50
86
  <% if attribute.name.to_s.match?(/email/) -%>
51
87
  <%= attribute.name %>: "workflow@example.com"<%= ',' if index < attributes.length - 1 %>
52
88
  <% elsif attribute.name.to_s.match?(/password/) -%>
53
89
  <%= attribute.name %>: "password123"<%= ',' if index < attributes.length - 1 %>
90
+ <% elsif attribute.name.to_s.match?(/\A(url|website|web_address|domain|domain_name)\z/i) -%>
91
+ <%= attribute.name %>: "https://workflow.example.com"<%= ',' if index < attributes.length - 1 %>
92
+ <% elsif attribute.name.to_s.end_with?('_type') -%>
93
+ <%= attribute.name %>: @<%= attribute.name.gsub('_type', '') %>.class.name<%= ',' if index < attributes.length - 1 %>
54
94
  <% else -%>
55
95
  <%= attribute.name %>: "Workflow Test <%= attribute.name.humanize %>"<%= ',' if index < attributes.length - 1 %>
56
96
  <% end -%>
@@ -64,11 +104,18 @@ class <%= class_name %>ApiTest < ActionDispatch::IntegrationTest
64
104
  <%= attribute.name %>: 100.50<%= ',' if index < attributes.length - 1 %>
65
105
  <% elsif attribute.type == :datetime -%>
66
106
  <%= attribute.name %>: Time.current<%= ',' if index < attributes.length - 1 %>
67
- <% elsif attribute.type == :json || attribute.name.to_s.match?(/^(meta|settings)$/) -%>
68
- <% if attribute.name == 'meta' -%>
69
- <%= attribute.name %>: { workflow_test_meta: "test_value", created_by: "integration_test", test_run_id: <%= rand(1000..9999) %> }<%= ',' if index < attributes.length - 1 %>
107
+ <% elsif attribute.type == :json || attribute.name.to_s.match?(/^(metadata|settings)$/) -%>
108
+ <% if attribute.name == 'metadata' -%>
109
+ <%= attribute.name %>: { resource_type: "<%= singular_table_name %>", category: "test", tags: ["integration", "auto_generated"], attributes: { priority: "medium", status_info: { verified: true, active: true } } }<%= ',' if index < attributes.length - 1 %>
70
110
  <% elsif attribute.name == 'settings' -%>
71
- <%= attribute.name %>: { dark_mode: true, money_format: "usd", language: "en", timezone: "UTC", notifications: { email: true, sms: false } }<%= ',' if index < attributes.length - 1 %>
111
+ <%= attribute.name %>: {
112
+ ui_preferences: {
113
+ theme: "dark",
114
+ language: "en",
115
+ notifications: { email: true, sms: false, push: true }
116
+ },
117
+ feature_flags: { beta_features: false, analytics: true }
118
+ }<%= ',' if index < attributes.length - 1 %>
72
119
  <% else -%>
73
120
  <%= attribute.name %>: {}<%= ',' if index < attributes.length - 1 %>
74
121
  <% end -%>
@@ -103,7 +150,7 @@ class <%= class_name %>ApiTest < ActionDispatch::IntegrationTest
103
150
 
104
151
  # Verify created <%= singular_table_name %> data
105
152
  assert_equal @organization.id, created_<%= singular_table_name %>['organization']['id']
106
- <% unless singular_table_name == 'user' -%>
153
+ <% if has_user_id && singular_table_name != 'user' -%>
107
154
  assert_equal @user.id, created_<%= singular_table_name %>['user']['id']
108
155
  <% end -%>
109
156
  <% attributes.each do |attribute| -%>
@@ -131,17 +178,28 @@ class <%= class_name %>ApiTest < ActionDispatch::IntegrationTest
131
178
  if created_<%= singular_table_name %>['<%= attribute.name %>'].present?
132
179
  assert_match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/, created_<%= singular_table_name %>['<%= attribute.name %>'], "<%= attribute.name %> should be ISO timestamp format")
133
180
  end
134
- <% elsif attribute.type == :json || attribute.name.to_s.match?(/^(meta|settings)$/) -%>
135
- <% if attribute.name == 'meta' -%>
136
- # Meta field should be a hash with test metadata
137
- assert_kind_of Hash, created_<%= singular_table_name %>['<%= attribute.name %>'], "Meta should be a JSON object"
138
- assert_includes created_<%= singular_table_name %>['<%= attribute.name %>'].keys, "workflow_test_meta", "Meta should contain test metadata"
181
+ <% elsif attribute.type == :json || attribute.name.to_s.match?(/^(metadata|settings)$/) -%>
182
+ <% if attribute.name == 'metadata' -%>
183
+ # Metadata field should be a hash with generic structure
184
+ assert_kind_of Hash, created_<%= singular_table_name %>['<%= attribute.name %>'], "Metadata should be a JSON object"
185
+ assert_includes created_<%= singular_table_name %>['<%= attribute.name %>'].keys, "resource_type", "Metadata should contain resource_type"
186
+ assert_includes created_<%= singular_table_name %>['<%= attribute.name %>'].keys, "category", "Metadata should contain category"
187
+ assert_includes created_<%= singular_table_name %>['<%= attribute.name %>'].keys, "tags", "Metadata should contain tags array"
188
+ assert_includes created_<%= singular_table_name %>['<%= attribute.name %>'].keys, "attributes", "Metadata should contain attributes object"
189
+ assert_kind_of Array, created_<%= singular_table_name %>['<%= attribute.name %>']['tags'], "Tags should be an array"
190
+ assert_kind_of Hash, created_<%= singular_table_name %>['<%= attribute.name %>']['attributes'], "Attributes should be a hash"
191
+ assert_includes created_<%= singular_table_name %>['<%= attribute.name %>']['attributes'].keys, "priority", "Attributes should contain priority"
192
+ assert_includes created_<%= singular_table_name %>['<%= attribute.name %>']['attributes'].keys, "status_info", "Attributes should contain status_info"
139
193
  <% elsif attribute.name == 'settings' -%>
140
- # Settings field should be a hash with user preferences
194
+ # Settings field should be a hash with generic structure
141
195
  assert_kind_of Hash, created_<%= singular_table_name %>['<%= attribute.name %>'], "Settings should be a JSON object"
142
- assert_includes created_<%= singular_table_name %>['<%= attribute.name %>'].keys, "dark_mode", "Settings should contain UI preferences"
143
- # Rails conventionally serializes booleans in JSON as strings
144
- assert_equal "true", created_<%= singular_table_name %>['<%= attribute.name %>']['dark_mode'], "Dark mode should be enabled in test settings"
196
+ assert_includes created_<%= singular_table_name %>['<%= attribute.name %>'].keys, "ui_preferences", "Settings should contain ui_preferences"
197
+ assert_includes created_<%= singular_table_name %>['<%= attribute.name %>'].keys, "feature_flags", "Settings should contain feature_flags"
198
+ assert_kind_of Hash, created_<%= singular_table_name %>['<%= attribute.name %>']['ui_preferences'], "UI preferences should be a hash"
199
+ assert_kind_of Hash, created_<%= singular_table_name %>['<%= attribute.name %>']['feature_flags'], "Feature flags should be a hash"
200
+ assert_includes created_<%= singular_table_name %>['<%= attribute.name %>']['ui_preferences'].keys, "theme", "UI preferences should contain theme"
201
+ assert_includes created_<%= singular_table_name %>['<%= attribute.name %>']['ui_preferences'].keys, "notifications", "UI preferences should contain notifications"
202
+ assert_kind_of Hash, created_<%= singular_table_name %>['<%= attribute.name %>']['ui_preferences']['notifications'], "Notifications should be a hash"
145
203
  <% else -%>
146
204
  assert_kind_of Hash, created_<%= singular_table_name %>['<%= attribute.name %>'], "<%= attribute.name %> should be a JSON object"
147
205
  <% end -%>
@@ -271,6 +329,10 @@ class <%= class_name %>ApiTest < ActionDispatch::IntegrationTest
271
329
  <%= attribute.name %>: "pagination#{i}@example.com"<%= ',' if index < attributes.length - 1 %>
272
330
  <% elsif attribute.name.include?('email') -%>
273
331
  <%= attribute.name %>: "pagination#{i}@example.com"<%= ',' if index < attributes.length - 1 %>
332
+ <% elsif attribute.name.to_s.match?(/\A(url|website|web_address|domain|domain_name)\z/i) -%>
333
+ <%= attribute.name %>: "https://pagination#{i}.example.com"<%= ',' if index < attributes.length - 1 %>
334
+ <% elsif attribute.name.to_s.end_with?('_type') -%>
335
+ <%= attribute.name %>: @<%= attribute.name.gsub('_type', '') %>.class.name<%= ',' if index < attributes.length - 1 %>
274
336
  <% else -%>
275
337
  <%= attribute.name %>: "Pagination Test <%= attribute.name.humanize %> #{i}"<%= ',' if index < attributes.length - 1 %>
276
338
  <% end -%>
@@ -356,11 +418,6 @@ class <%= class_name %>ApiTest < ActionDispatch::IntegrationTest
356
418
  # Test 4: Tenancy context behavior based on configuration
357
419
  require_org_id = PropelAuthentication.configuration.require_organization_id
358
420
  require_user_id = PropelAuthentication.configuration.require_user_id
359
- <%
360
- # Check attributes directly instead of database columns (more reliable during generation)
361
- has_agency_id = attributes.any? { |attr| attr.name == 'agency' && attr.type == :references }
362
- has_user_id = attributes.any? { |attr| attr.name == 'user' && attr.type == :references }
363
- -%>
364
421
 
365
422
  <% if has_agency_id -%>
366
423
  # Model with agency - provide agency_id but test missing organization_id
@@ -368,7 +425,7 @@ class <%= class_name %>ApiTest < ActionDispatch::IntegrationTest
368
425
  unique_id = "#{Time.current.to_i}_#{SecureRandom.hex(6)}"
369
426
  test_params = { email_address: "tenancy_test_#{unique_id}@example.com", username: "tenancy_test_#{unique_id}", password: "password123", agency_id: @agency.id }
370
427
  <% else -%>
371
- test_params = { title: "Test <%= class_name %>", agency_id: @agency.id }
428
+ test_params = { title: "Test <%= class_name %>", agency_id: @agency.id<% polymorphic_associations.each do |assoc| -%>, <%= assoc[:field_name] %>_id: @<%= assoc[:field_name] %>.id, <%= assoc[:field_name] %>_type: @<%= assoc[:field_name] %>.class.name<% end -%> }
372
429
  <% end -%>
373
430
  <% else -%>
374
431
  # Model without agency - test missing organization_id only
@@ -376,7 +433,7 @@ class <%= class_name %>ApiTest < ActionDispatch::IntegrationTest
376
433
  unique_id = "#{Time.current.to_i}_#{SecureRandom.hex(6)}"
377
434
  test_params = { email_address: "tenancy_test_#{unique_id}@example.com", username: "tenancy_test_#{unique_id}", password: "password123" }
378
435
  <% else -%>
379
- test_params = { title: "Test <%= class_name %>" }
436
+ test_params = { title: "Test <%= class_name %>"<% polymorphic_associations.each do |assoc| -%>, <%= assoc[:field_name] %>_id: @<%= assoc[:field_name] %>.id, <%= assoc[:field_name] %>_type: @<%= assoc[:field_name] %>.class.name<% end -%> }
380
437
  <% end -%>
381
438
  <% end -%>
382
439
 
@@ -386,7 +443,7 @@ class <%= class_name %>ApiTest < ActionDispatch::IntegrationTest
386
443
 
387
444
  if require_org_id<% if has_agency_id %> || false<% end -%> # Agency models test organization_id behavior
388
445
  # Strict mode: Should return 422 when tenancy context is required but missing
389
- assert_response :unprocessable_entity
446
+ assert_response :unprocessable_content
390
447
 
391
448
  error_response = JSON.parse(response.body)
392
449
  assert_includes error_response.keys, 'errors'
@@ -446,7 +503,7 @@ class <%= class_name %>ApiTest < ActionDispatch::IntegrationTest
446
503
  headers: @auth_headers
447
504
  <% else -%>
448
505
  post <%= api_route_helper %>_url,
449
- params: { data: { organization_id: @organization.id, agency_id: 99999, title: "Test <%= class_name %>" } },
506
+ params: { data: { organization_id: @organization.id, agency_id: 99999, title: "Test <%= class_name %>"<% polymorphic_associations.each do |assoc| -%>, <%= assoc[:field_name] %>_id: @<%= assoc[:field_name] %>.id, <%= assoc[:field_name] %>_type: @<%= assoc[:field_name] %>.class.name<% end -%> } },
450
507
  headers: @auth_headers
451
508
  <% end -%>
452
509
 
@@ -465,7 +522,7 @@ class <%= class_name %>ApiTest < ActionDispatch::IntegrationTest
465
522
  headers: @auth_headers
466
523
  <% else -%>
467
524
  post <%= api_route_helper %>_url,
468
- params: { data: { organization_id: 99999, title: "Test <%= class_name %>" } },
525
+ params: { data: { organization_id: 99999, title: "Test <%= class_name %>"<% polymorphic_associations.each do |assoc| -%>, <%= assoc[:field_name] %>_id: @<%= assoc[:field_name] %>.id, <%= assoc[:field_name] %>_type: @<%= assoc[:field_name] %>.class.name<% end -%> } },
469
526
  headers: @auth_headers
470
527
  <% end -%>
471
528
 
@@ -540,6 +597,10 @@ class <%= class_name %>ApiTest < ActionDispatch::IntegrationTest
540
597
  <%= attribute.name %>: "org1_<%= attribute.name %>@example.com"<%= ',' if index < attributes.length - 1 %>
541
598
  <% elsif attribute.name.include?('password') -%>
542
599
  <%= attribute.name %>: "org1_secure_password"<%= ',' if index < attributes.length - 1 %>
600
+ <% elsif attribute.name.to_s.match?(/\A(url|website|web_address|domain|domain_name)\z/i) -%>
601
+ <%= attribute.name %>: "https://org1.example.com"<%= ',' if index < attributes.length - 1 %>
602
+ <% elsif attribute.name.to_s.end_with?('_type') -%>
603
+ <%= attribute.name %>: @<%= attribute.name.gsub('_type', '') %>.class.name<%= ',' if index < attributes.length - 1 %>
543
604
  <% else -%>
544
605
  <%= attribute.name %>: "Org 1 <%= attribute.name.humanize %>"<%= ',' if index < attributes.length - 1 %>
545
606
  <% end -%>
@@ -551,11 +612,19 @@ class <%= class_name %>ApiTest < ActionDispatch::IntegrationTest
551
612
  <%= attribute.name %>: true<%= ',' if index < attributes.length - 1 %>
552
613
  <% elsif attribute.type == :decimal || attribute.type == :float -%>
553
614
  <%= attribute.name %>: 11.1<%= ',' if index < attributes.length - 1 %>
554
- <% elsif attribute.type == :json || attribute.name.to_s.match?(/^(meta|settings)$/) -%>
555
- <% if attribute.name == 'meta' -%>
556
- <%= attribute.name %>: { org1_test_meta: "org1_value", tenant: "org1", test_scenario: "multi_tenancy" }<%= ',' if index < attributes.length - 1 %>
615
+ <% elsif attribute.type == :json || attribute.name.to_s.match?(/^(metadata|settings)$/) -%>
616
+ <% if attribute.name == 'metadata' -%>
617
+ <%= attribute.name %>: {
618
+ resource_type: "<%= singular_table_name %>",
619
+ category: "employee",
620
+ tags: ["org1", "department_test"],
621
+ attributes: {
622
+ priority: "medium",
623
+ status_info: { verified: true, active: true }
624
+ }
625
+ }<%= ',' if index < attributes.length - 1 %>
557
626
  <% elsif attribute.name == 'settings' -%>
558
- <%= attribute.name %>: { dark_mode: false, money_format: "usd", language: "en", org1_preference: true }<%= ',' if index < attributes.length - 1 %>
627
+ <%= attribute.name %>: { dark_mode: false, notifications: { email: true, sms: true } }<%= ',' if index < attributes.length - 1 %>
559
628
  <% else -%>
560
629
  <%= attribute.name %>: {}<%= ',' if index < attributes.length - 1 %>
561
630
  <% end -%>
@@ -578,6 +647,10 @@ class <%= class_name %>ApiTest < ActionDispatch::IntegrationTest
578
647
  <%= attribute.name %>: "org2_<%= attribute.name %>@example.com"<%= ',' if index < attributes.length - 1 %>
579
648
  <% elsif attribute.name.include?('password') -%>
580
649
  <%= attribute.name %>: "org2_secure_password"<%= ',' if index < attributes.length - 1 %>
650
+ <% elsif attribute.name.to_s.match?(/\A(url|website|web_address|domain|domain_name)\z/i) -%>
651
+ <%= attribute.name %>: "https://org2.example.com"<%= ',' if index < attributes.length - 1 %>
652
+ <% elsif attribute.name.to_s.end_with?('_type') -%>
653
+ <%= attribute.name %>: @<%= attribute.name.gsub('_type', '') %>.class.name<%= ',' if index < attributes.length - 1 %>
581
654
  <% else -%>
582
655
  <%= attribute.name %>: "Org 2 <%= attribute.name.humanize %>"<%= ',' if index < attributes.length - 1 %>
583
656
  <% end -%>
@@ -589,11 +662,19 @@ class <%= class_name %>ApiTest < ActionDispatch::IntegrationTest
589
662
  <%= attribute.name %>: false<%= ',' if index < attributes.length - 1 %>
590
663
  <% elsif attribute.type == :decimal || attribute.type == :float -%>
591
664
  <%= attribute.name %>: 22.2<%= ',' if index < attributes.length - 1 %>
592
- <% elsif attribute.type == :json || attribute.name.to_s.match?(/^(meta|settings)$/) -%>
593
- <% if attribute.name == 'meta' -%>
594
- <%= attribute.name %>: { org2_test_meta: "org2_value", tenant: "org2", test_scenario: "multi_tenancy" }<%= ',' if index < attributes.length - 1 %>
665
+ <% elsif attribute.type == :json || attribute.name.to_s.match?(/^(metadata|settings)$/) -%>
666
+ <% if attribute.name == 'metadata' -%>
667
+ <%= attribute.name %>: {
668
+ resource_type: "<%= singular_table_name %>",
669
+ category: "manager",
670
+ tags: ["org2", "department_test"],
671
+ attributes: {
672
+ priority: "high",
673
+ status_info: { verified: true, active: true }
674
+ }
675
+ }<%= ',' if index < attributes.length - 1 %>
595
676
  <% elsif attribute.name == 'settings' -%>
596
- <%= attribute.name %>: { dark_mode: true, money_format: "eur", language: "fr", org2_preference: true }<%= ',' if index < attributes.length - 1 %>
677
+ <%= attribute.name %>: { dark_mode: true, notifications: { email: false, sms: true } }<%= ',' if index < attributes.length - 1 %>
597
678
  <% else -%>
598
679
  <%= attribute.name %>: {}<%= ',' if index < attributes.length - 1 %>
599
680
  <% end -%>
@@ -667,7 +748,7 @@ class <%= class_name %>ApiTest < ActionDispatch::IntegrationTest
667
748
 
668
749
  # Verify core fields are present
669
750
  assert_includes <%= singular_table_name %>_data.keys, 'id'
670
- <% unless options[:skip_tenancy] || class_name == 'Organization' -%>
751
+ <% if has_organization_reference? -%>
671
752
  assert_includes <%= singular_table_name %>_data.keys, 'organization'
672
753
 
673
754
  # Verify associations are properly included
@@ -726,6 +807,10 @@ if attributes.any? { |attr| attr.type == :references && attr.name == 'user' } &&
726
807
  <%= attribute.name %>: "concurrent@example.com"<%= ',' if index < attributes.length - 1 %>
727
808
  <% elsif attribute.name.include?('email') -%>
728
809
  <%= attribute.name %>: "concurrent@example.com"<%= ',' if index < attributes.length - 1 %>
810
+ <% elsif attribute.name.to_s.match?(/\A(url|website|web_address|domain|domain_name)\z/i) -%>
811
+ <%= attribute.name %>: "https://concurrent.example.com"<%= ',' if index < attributes.length - 1 %>
812
+ <% elsif attribute.name.to_s.end_with?('_type') -%>
813
+ <%= attribute.name %>: @<%= attribute.name.gsub('_type', '') %>.class.name<%= ',' if index < attributes.length - 1 %>
729
814
  <% else -%>
730
815
  <%= attribute.name %>: "Concurrent Test <%= attribute.name.humanize %>"<%= ',' if index < attributes.length - 1 %>
731
816
  <% end -%>
@@ -737,11 +822,26 @@ if attributes.any? { |attr| attr.type == :references && attr.name == 'user' } &&
737
822
  <%= attribute.name %>: true<%= ',' if index < attributes.length - 1 %>
738
823
  <% elsif attribute.type == :decimal || attribute.type == :float -%>
739
824
  <%= attribute.name %>: 50.5<%= ',' if index < attributes.length - 1 %>
740
- <% elsif attribute.type == :json || attribute.name.to_s.match?(/^(meta|settings)$/) -%>
741
- <% if attribute.name == 'meta' -%>
742
- <%= attribute.name %>: { concurrent_test_meta: "concurrent_value", test_type: "concurrent", thread_id: 1 }<%= ',' if index < attributes.length - 1 %>
825
+ <% elsif attribute.type == :json || attribute.name.to_s.match?(/^(metadata|settings)$/) -%>
826
+ <% if attribute.name == 'metadata' -%>
827
+ <%= attribute.name %>: {
828
+ resource_type: "<%= singular_table_name %>",
829
+ category: "admin",
830
+ tags: ["concurrent", "test"],
831
+ attributes: {
832
+ priority: "high",
833
+ status_info: { verified: true, active: true }
834
+ }
835
+ }<%= ',' if index < attributes.length - 1 %>
743
836
  <% elsif attribute.name == 'settings' -%>
744
- <%= attribute.name %>: { dark_mode: true, money_format: "gbp", language: "en", concurrent_mode: true }<%= ',' if index < attributes.length - 1 %>
837
+ <%= attribute.name %>: {
838
+ ui_preferences: {
839
+ theme: "dark",
840
+ language: "en",
841
+ notifications: { email: true, sms: false, push: true }
842
+ },
843
+ feature_flags: { beta_features: false, analytics: true }
844
+ }<%= ',' if index < attributes.length - 1 %>
745
845
  <% else -%>
746
846
  <%= attribute.name %>: {}<%= ',' if index < attributes.length - 1 %>
747
847
  <% end -%>
@@ -826,6 +926,10 @@ if attributes.any? { |attr| attr.type == :references && attr.name == 'user' } &&
826
926
  <%= attribute.name %>: "bulk#{i}@example.com"<%= ',' if index < attributes.length - 1 %>
827
927
  <% elsif attribute.name.include?('email') -%>
828
928
  <%= attribute.name %>: "bulk#{i}@example.com"<%= ',' if index < attributes.length - 1 %>
929
+ <% elsif attribute.name.to_s.match?(/\A(url|website|web_address|domain|domain_name)\z/i) -%>
930
+ <%= attribute.name %>: "https://bulk#{i}.example.com"<%= ',' if index < attributes.length - 1 %>
931
+ <% elsif attribute.name.to_s.end_with?('_type') -%>
932
+ <%= attribute.name %>: @<%= attribute.name.gsub('_type', '') %>.class.name<%= ',' if index < attributes.length - 1 %>
829
933
  <% else -%>
830
934
  <%= attribute.name %>: "Bulk Test <%= attribute.name.humanize %> #{i}"<%= ',' if index < attributes.length - 1 %>
831
935
  <% end -%>
@@ -837,11 +941,19 @@ if attributes.any? { |attr| attr.type == :references && attr.name == 'user' } &&
837
941
  <%= attribute.name %>: i.even?<%= ',' if index < attributes.length - 1 %>
838
942
  <% elsif attribute.type == :decimal || attribute.type == :float -%>
839
943
  <%= attribute.name %>: i * 10.5<%= ',' if index < attributes.length - 1 %>
840
- <% elsif attribute.type == :json || attribute.name.to_s.match?(/^(meta|settings)$/) -%>
841
- <% if attribute.name == 'meta' -%>
842
- <%= attribute.name %>: { bulk_test_meta: "bulk_#{i}", batch_number: i, test_type: "bulk_operations" }<%= ',' if index < attributes.length - 1 %>
944
+ <% elsif attribute.type == :json || attribute.name.to_s.match?(/^(metadata|settings)$/) -%>
945
+ <% if attribute.name == 'metadata' -%>
946
+ <%= attribute.name %>: {
947
+ resource_type: "<%= singular_table_name %>",
948
+ category: "employee",
949
+ tags: ["bulk", "test_#{i}"],
950
+ attributes: {
951
+ priority: "medium",
952
+ status_info: { verified: true, active: true }
953
+ }
954
+ }<%= ',' if index < attributes.length - 1 %>
843
955
  <% elsif attribute.name == 'settings' -%>
844
- <%= attribute.name %>: { dark_mode: i.even?, money_format: (i.even? ? "usd" : "eur"), language: "en", "bulk_preference_#{i}": true }<%= ',' if index < attributes.length - 1 %>
956
+ <%= attribute.name %>: { dark_mode: i.even?, notifications: { email: true, sms: i.even? } }<%= ',' if index < attributes.length - 1 %>
845
957
  <% else -%>
846
958
  <%= attribute.name %>: {}<%= ',' if index < attributes.length - 1 %>
847
959
  <% end -%>
@@ -8,6 +8,16 @@ class <%= class_name %>Test < ActiveSupport::TestCase
8
8
  @organization = organizations(:acme_org)
9
9
  @user = users(:john_user)
10
10
  @agency = agencies(:marketing_agency)
11
+ <% polymorphic_associations.each do |assoc| -%>
12
+ <% assoc[:parent_types].each_with_index do |parent_type, index| -%>
13
+ <% if index == 0 -%>
14
+ @<%= parent_type.underscore %> = <%= parent_type.underscore.pluralize %>(<%= case parent_type.underscore; when 'agency'; ':marketing_agency'; when 'user'; ':john_user'; when 'organization'; ':acme_org'; else; ':one'; end %>)
15
+ @<%= assoc[:field_name] %> = @<%= parent_type.underscore %> # Default polymorphic parent for tests
16
+ <% else -%>
17
+ @<%= parent_type.underscore %> = <%= parent_type.underscore.pluralize %>(<%= case parent_type.underscore; when 'agency'; ':marketing_agency'; when 'user'; ':john_user'; when 'organization'; ':acme_org'; else; ':one'; end %>)
18
+ <% end -%>
19
+ <% end -%>
20
+ <% end -%>
11
21
  <% if singular_table_name == 'organization' -%>
12
22
  @<%= singular_table_name %> = <%= table_name %>(:acme_org)
13
23
  <% elsif singular_table_name == 'user' -%>
@@ -24,7 +34,17 @@ class <%= class_name %>Test < ActiveSupport::TestCase
24
34
 
25
35
  test "should belong to <%= attribute.name %>" do
26
36
  assert_respond_to @<%= singular_table_name %>, :<%= attribute.name %>
37
+ <% if attribute.respond_to?(:polymorphic?) && attribute.polymorphic? -%>
38
+ assert @<%= singular_table_name %>.<%= attribute.name %>.present?
39
+ # Polymorphic association - check against valid parent types
40
+ <% polymorphic_associations.each do |assoc| -%>
41
+ <% if assoc[:field_name] == attribute.name -%>
42
+ assert(<%= assoc[:parent_types].map { |type| "@#{singular_table_name}.#{attribute.name}.is_a?(#{type})" }.join(' || ') %>)
43
+ <% end -%>
44
+ <% end -%>
45
+ <% else -%>
27
46
  assert_kind_of <%= attribute.name.camelize %>, @<%= singular_table_name %>.<%= attribute.name %>
47
+ <% end -%>
28
48
  end
29
49
 
30
50
  test "should be invalid without <%= attribute.name %>" do
data/lib/propel_api.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module PropelApi
2
- VERSION = "0.2.1"
2
+ VERSION = "0.3.1"
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.2.1
4
+ version: 0.3.1
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-09-02 00:00:00.000000000 Z
11
+ date: 2025-09-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -30,6 +30,26 @@ dependencies:
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
32
  version: '9.0'
33
+ - !ruby/object:Gem::Dependency
34
+ name: pagy
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '8.0'
40
+ - - "<"
41
+ - !ruby/object:Gem::Version
42
+ version: '10.0'
43
+ type: :runtime
44
+ prerelease: false
45
+ version_requirements: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: '8.0'
50
+ - - "<"
51
+ - !ruby/object:Gem::Version
52
+ version: '10.0'
33
53
  description: A comprehensive Rails generator that creates complete API resources with
34
54
  models, controllers, tests, and realistic seed data. Supports both JSON Facet and
35
55
  Graphiti serialization engines.
@@ -53,9 +73,11 @@ files:
53
73
  - lib/generators/propel_api/install/install_generator.rb
54
74
  - lib/generators/propel_api/resource/resource_generator.rb
55
75
  - lib/generators/propel_api/templates/config/propel_api.rb.tt
76
+ - lib/generators/propel_api/templates/controllers/api_base_controller.rb
56
77
  - lib/generators/propel_api/templates/controllers/api_controller_graphiti.rb
57
78
  - lib/generators/propel_api/templates/controllers/api_controller_propel_facets.rb
58
79
  - lib/generators/propel_api/templates/controllers/example_controller.rb.tt
80
+ - lib/generators/propel_api/templates/errors/propel_api_csrf_error.rb
59
81
  - lib/generators/propel_api/templates/scaffold/facet_controller_template.rb.tt
60
82
  - lib/generators/propel_api/templates/scaffold/facet_model_template.rb.tt
61
83
  - lib/generators/propel_api/templates/scaffold/graphiti_controller_template.rb.tt