belt 0.2.17 → 0.3.15

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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +241 -0
  3. data/README.md +42 -3
  4. data/lib/belt/action_router.rb +34 -19
  5. data/lib/belt/cli/app_detection.rb +30 -3
  6. data/lib/belt/cli/auth_command.rb +395 -0
  7. data/lib/belt/cli/console_command.rb +17 -2
  8. data/lib/belt/cli/deploy_command.rb +51 -17
  9. data/lib/belt/cli/destroy_command.rb +62 -25
  10. data/lib/belt/cli/doctor_command.rb +166 -22
  11. data/lib/belt/cli/environment_command.rb +12 -0
  12. data/lib/belt/cli/explain_command.rb +112 -0
  13. data/lib/belt/cli/frontend_command.rb +31 -10
  14. data/lib/belt/cli/frontend_deploy_command.rb +110 -21
  15. data/lib/belt/cli/frontend_env_command.rb +56 -19
  16. data/lib/belt/cli/frontend_env_map.rb +30 -10
  17. data/lib/belt/cli/frontend_registry.rb +373 -0
  18. data/lib/belt/cli/frontend_setup_command.rb +71 -5
  19. data/lib/belt/cli/generate_command.rb +61 -42
  20. data/lib/belt/cli/index_command.rb +192 -0
  21. data/lib/belt/cli/lambda_config_command.rb +9 -3
  22. data/lib/belt/cli/routes_command/request_model_inference.rb +131 -0
  23. data/lib/belt/cli/routes_command/route_inference.rb +4 -3
  24. data/lib/belt/cli/routes_command.rb +31 -19
  25. data/lib/belt/cli/server_command.rb +28 -15
  26. data/lib/belt/cli/tables_command.rb +3 -1
  27. data/lib/belt/cli/terraform_command.rb +9 -0
  28. data/lib/belt/cli/views_command.rb +15 -15
  29. data/lib/belt/cli/zip_artifact_builder.rb +199 -0
  30. data/lib/belt/cli.rb +17 -9
  31. data/lib/belt/docs/backups.md +101 -0
  32. data/lib/belt/docs/console.md +65 -0
  33. data/lib/belt/docs/controllers.md +155 -0
  34. data/lib/belt/docs/deployment.md +166 -0
  35. data/lib/belt/docs/frontend.md +103 -0
  36. data/lib/belt/docs/generators.md +137 -0
  37. data/lib/belt/docs/lambda_handler.md +105 -0
  38. data/lib/belt/docs/models.md +160 -0
  39. data/lib/belt/docs/observability.md +94 -0
  40. data/lib/belt/docs/plugins.md +94 -0
  41. data/lib/belt/docs/routing.md +138 -0
  42. data/lib/belt/docs/structure.md +96 -0
  43. data/lib/belt/inflector.rb +8 -0
  44. data/lib/belt/route_dsl.rb +318 -100
  45. data/lib/belt/version.rb +1 -1
  46. data/lib/templates/frontend/react/env.yml.example +2 -2
  47. data/lib/templates/generate/auth/cognito.tf.erb +71 -0
  48. data/lib/templates/generate/auth/cognito_outputs.tf.erb +19 -0
  49. data/lib/templates/generate/auth/frontend/ConfirmEmail.jsx +55 -0
  50. data/lib/templates/generate/auth/frontend/Login.jsx +79 -0
  51. data/lib/templates/generate/auth/frontend/ProtectedRoute.jsx +9 -0
  52. data/lib/templates/generate/auth/frontend/SignUp.jsx +58 -0
  53. data/lib/templates/generate/auth/frontend/apiClient.js +34 -0
  54. data/lib/templates/generate/auth/frontend/auth.css +157 -0
  55. data/lib/templates/generate/auth/frontend/auth.js +134 -0
  56. data/lib/templates/generate/model.rb.erb +4 -6
  57. data/lib/templates/module/frontend.tf.erb +49 -43
  58. data/lib/templates/module/outputs.tf.erb +1 -1
  59. data/lib/templates/new_app/AGENTS.md.erb +170 -20
  60. data/lib/templates/new_app/config/lambda/api.yml.erb +1 -0
  61. data/lib/templates/new_app/config/routes.rb.erb +8 -3
  62. data/lib/templates/new_app/lambda/api.rb.erb +1 -1
  63. metadata +56 -1
@@ -52,48 +52,46 @@ module Belt
52
52
 
53
53
  class NestedResourceBuilder
54
54
  def initialize(gateway, prefix, collection_prefix, inherited_tables: [], inherited_auth: nil, # rubocop:disable Metrics/ParameterLists
55
- inherited_controller: nil)
55
+ inherited_controller: nil, inherited_lambda: nil)
56
56
  @gateway = gateway
57
57
  @prefix = prefix
58
58
  @collection_prefix = collection_prefix
59
59
  @inherited_tables = inherited_tables
60
60
  @inherited_auth = inherited_auth
61
61
  @inherited_controller = inherited_controller
62
+ @inherited_lambda = inherited_lambda
62
63
  end
63
64
 
64
- def resources(name, options = {})
65
+ def resources(name, options = {}, &block)
65
66
  resource_name = name.to_s
66
67
  singular = @gateway.send(:singularize, resource_name)
67
68
  param_name = options[:param] || "#{singular}_id"
69
+ options = options.merge(tables: [resource_name.to_sym]) unless options.key?(:tables)
68
70
  options = merge_inherited_options(options)
69
- options = @gateway.send(:auto_infer_tables, resource_name, options)
70
71
  resource_options = options.merge(route_type: :resources)
71
72
  actions = @gateway.send(:determine_actions, options)
72
73
 
73
- @gateway.send(:add_route, :get, "#{@prefix}/#{resource_name}", resource_options) if actions.include?(:index)
74
- @gateway.send(:add_route, :post, "#{@prefix}/#{resource_name}", resource_options) if actions.include?(:create)
75
- if actions.include?(:show)
76
- @gateway.send(:add_route, :get, "#{@prefix}/#{resource_name}/{#{param_name}}",
77
- resource_options)
78
- end
79
- if actions.include?(:update)
80
- @gateway.send(:add_route, :put, "#{@prefix}/#{resource_name}/{#{param_name}}",
81
- resource_options)
82
- end
83
- return unless actions.include?(:destroy)
74
+ add_nested_resource_routes(resource_name, param_name, resource_options, actions)
75
+ return unless block
84
76
 
85
- @gateway.send(:add_route, :delete, "#{@prefix}/#{resource_name}/{#{param_name}}",
86
- resource_options)
77
+ nested_member_prefix = "#{@prefix}/#{resource_name}/{#{param_name}}"
78
+ nested_collection_prefix = "#{@prefix}/#{resource_name}"
79
+ nested_builder = NestedResourceBuilder.new(@gateway, nested_member_prefix, nested_collection_prefix,
80
+ inherited_tables: Array(options[:tables] || []),
81
+ inherited_auth: options[:auth] || @inherited_auth,
82
+ inherited_controller: @inherited_controller,
83
+ inherited_lambda: @inherited_lambda)
84
+ nested_builder.instance_eval(&block)
87
85
  end
88
86
 
89
87
  def member(&)
90
88
  MemberCollectionBuilder.new(@gateway, @prefix, @inherited_tables, @inherited_auth,
91
- @inherited_controller).instance_eval(&)
89
+ @inherited_controller, @inherited_lambda).instance_eval(&)
92
90
  end
93
91
 
94
92
  def collection(&)
95
93
  MemberCollectionBuilder.new(@gateway, @collection_prefix, @inherited_tables,
96
- @inherited_auth, @inherited_controller).instance_eval(&)
94
+ @inherited_auth, @inherited_controller, @inherited_lambda).instance_eval(&)
97
95
  end
98
96
 
99
97
  %i[get post put delete patch].each do |method|
@@ -107,6 +105,40 @@ module Belt
107
105
 
108
106
  private
109
107
 
108
+ def add_nested_resource_routes(resource_name, param_name, resource_options, actions)
109
+ if actions.include?(:index)
110
+ @gateway.send(:add_route, :get, "#{@prefix}/#{resource_name}",
111
+ resolve_request_model_for(resource_options, :index))
112
+ end
113
+ if actions.include?(:create)
114
+ @gateway.send(:add_route, :post, "#{@prefix}/#{resource_name}",
115
+ resolve_request_model_for(resource_options, :create))
116
+ end
117
+ if actions.include?(:show)
118
+ @gateway.send(:add_route, :get, "#{@prefix}/#{resource_name}/{#{param_name}}",
119
+ resolve_request_model_for(resource_options, :show))
120
+ end
121
+ if actions.include?(:update)
122
+ @gateway.send(:add_route, :put, "#{@prefix}/#{resource_name}/{#{param_name}}",
123
+ resolve_request_model_for(resource_options, :update))
124
+ end
125
+ return unless actions.include?(:destroy)
126
+
127
+ @gateway.send(:add_route, :delete, "#{@prefix}/#{resource_name}/{#{param_name}}",
128
+ resolve_request_model_for(resource_options, :destroy))
129
+ end
130
+
131
+ def resolve_request_model_for(options, action)
132
+ rm = options[:request_model]
133
+ case rm
134
+ when Hash
135
+ resolved = rm[action] || rm[action.to_s]
136
+ options.merge(request_model: resolved)
137
+ else
138
+ options
139
+ end
140
+ end
141
+
110
142
  def merge_inherited_options(options)
111
143
  result = options.dup
112
144
  if @inherited_tables.any?
@@ -115,17 +147,20 @@ module Belt
115
147
  end
116
148
  result[:auth] ||= @inherited_auth if @inherited_auth
117
149
  result[:controller] ||= @inherited_controller if @inherited_controller
150
+ result[:lambda] ||= @inherited_lambda if @inherited_lambda
118
151
  result
119
152
  end
120
153
  end
121
154
 
122
155
  class MemberCollectionBuilder
123
- def initialize(gateway, prefix, inherited_tables, inherited_auth, inherited_controller = nil)
156
+ def initialize(gateway, prefix, inherited_tables, inherited_auth, # rubocop:disable Metrics/ParameterLists
157
+ inherited_controller = nil, inherited_lambda = nil)
124
158
  @gateway = gateway
125
159
  @prefix = prefix
126
160
  @inherited_tables = inherited_tables
127
161
  @inherited_auth = inherited_auth
128
162
  @inherited_controller = inherited_controller
163
+ @inherited_lambda = inherited_lambda
129
164
  end
130
165
 
131
166
  %i[get post put delete patch].each do |method|
@@ -146,6 +181,7 @@ module Belt
146
181
  end
147
182
  result[:auth] ||= @inherited_auth if @inherited_auth
148
183
  result[:controller] ||= @inherited_controller if @inherited_controller
184
+ result[:lambda] ||= @inherited_lambda if @inherited_lambda
149
185
  result
150
186
  end
151
187
  end
@@ -156,7 +192,7 @@ module Belt
156
192
  def initialize(name, options = {})
157
193
  @name = name.to_s
158
194
  @routes = []
159
- @default_auth = options[:auth] || :cognito
195
+ @default_auth = options[:auth] || :none
160
196
  @default_lambda = options[:lambda] || name
161
197
  @default_cors = options.fetch(:cors, true)
162
198
  @default_tables = Array(options[:tables] || [])
@@ -184,11 +220,7 @@ module Belt
184
220
  resource_options = options.merge(route_type: :resources)
185
221
  actions = determine_actions(options)
186
222
 
187
- add_route(:get, "/#{resource_name}", resource_options) if actions.include?(:index)
188
- add_route(:post, "/#{resource_name}", resource_options) if actions.include?(:create)
189
- add_route(:get, "/#{resource_name}/{#{param_name}}", resource_options) if actions.include?(:show)
190
- add_route(:put, "/#{resource_name}/{#{param_name}}", resource_options) if actions.include?(:update)
191
- add_route(:delete, "/#{resource_name}/{#{param_name}}", resource_options) if actions.include?(:destroy)
223
+ add_resource_routes(resource_name, param_name, resource_options, actions)
192
224
 
193
225
  return unless block_given?
194
226
 
@@ -197,9 +229,11 @@ module Belt
197
229
  resource_tables = Array(options[:tables] || [])
198
230
  inherited_tables = (@default_tables + resource_tables).uniq
199
231
  inherited_auth = options[:auth] || @default_auth
232
+ inherited_lambda = options[:lambda]
200
233
  nested_builder = NestedResourceBuilder.new(self, member_prefix, collection_prefix,
201
234
  inherited_tables: inherited_tables,
202
- inherited_auth: inherited_auth)
235
+ inherited_auth: inherited_auth,
236
+ inherited_lambda: inherited_lambda)
203
237
  nested_builder.instance_eval(&)
204
238
  end
205
239
 
@@ -208,10 +242,22 @@ module Belt
208
242
  actions = determine_actions(options, default: %i[show update destroy])
209
243
  resource_options = options.merge(route_type: :resource)
210
244
 
211
- add_route(:get, "/#{resource_name}", resource_options) if actions.include?(:show)
212
- add_route(:put, "/#{resource_name}", resource_options) if actions.include?(:update)
213
- add_route(:delete, "/#{resource_name}", resource_options) if actions.include?(:destroy)
214
- add_route(:post, "/#{resource_name}", resource_options) if actions.include?(:create)
245
+ if actions.include?(:show)
246
+ add_route(:get, "/#{resource_name}",
247
+ resolve_request_model_for(resource_options, :show))
248
+ end
249
+ if actions.include?(:update)
250
+ add_route(:put, "/#{resource_name}",
251
+ resolve_request_model_for(resource_options, :update))
252
+ end
253
+ if actions.include?(:destroy)
254
+ add_route(:delete, "/#{resource_name}",
255
+ resolve_request_model_for(resource_options, :destroy))
256
+ end
257
+ return unless actions.include?(:create)
258
+
259
+ add_route(:post, "/#{resource_name}",
260
+ resolve_request_model_for(resource_options, :create))
215
261
  end
216
262
 
217
263
  private
@@ -253,6 +299,39 @@ module Belt
253
299
  options.merge(tables: [resource_name.to_sym])
254
300
  end
255
301
 
302
+ def add_resource_routes(resource_name, param_name, resource_options, actions)
303
+ if actions.include?(:index)
304
+ add_route(:get, "/#{resource_name}",
305
+ resolve_request_model_for(resource_options, :index))
306
+ end
307
+ if actions.include?(:create)
308
+ add_route(:post, "/#{resource_name}",
309
+ resolve_request_model_for(resource_options, :create))
310
+ end
311
+ if actions.include?(:show)
312
+ add_route(:get, "/#{resource_name}/{#{param_name}}",
313
+ resolve_request_model_for(resource_options, :show))
314
+ end
315
+ if actions.include?(:update)
316
+ add_route(:put, "/#{resource_name}/{#{param_name}}",
317
+ resolve_request_model_for(resource_options, :update))
318
+ end
319
+ return unless actions.include?(:destroy)
320
+
321
+ add_route(:delete, "/#{resource_name}/{#{param_name}}", resolve_request_model_for(resource_options, :destroy))
322
+ end
323
+
324
+ def resolve_request_model_for(options, action)
325
+ rm = options[:request_model]
326
+ case rm
327
+ when Hash
328
+ resolved = rm[action] || rm[action.to_s]
329
+ options.merge(request_model: resolved)
330
+ else
331
+ options
332
+ end
333
+ end
334
+
256
335
  def determine_actions(options, default: %i[index create show update destroy])
257
336
  if options[:only]
258
337
  Array(options[:only])
@@ -282,11 +361,15 @@ module Belt
282
361
  @dsl
283
362
  end
284
363
 
285
- def namespace(name, options = {}, &)
286
- gateway = Belt::ApiGateway.new(name, options)
287
- RouteBuilder.new(gateway).instance_eval(&) if block_given?
288
- @dsl.api_gateways << gateway
364
+ # Primary DSL keyword: defines an API Gateway with a default Lambda function.
365
+ def gateway(name, options = {}, &)
366
+ gw = Belt::ApiGateway.new(name, options)
367
+ RouteBuilder.new(gw).instance_eval(&) if block_given?
368
+ @dsl.api_gateways << gw
289
369
  end
370
+
371
+ # Legacy alias — existing routes files using `namespace` still work.
372
+ alias namespace gateway
290
373
  end
291
374
 
292
375
  def routes
@@ -302,11 +385,40 @@ module Belt
302
385
  @gateway = gateway
303
386
  @scope_prefix = ''
304
387
  @scope_module = nil
388
+ @lambda_target = nil
305
389
  @scope_auth = nil
306
390
  @scope_tables = []
307
391
  @scope_controller = nil
308
392
  end
309
393
 
394
+ # Rails-like `namespace` — adds both a path prefix AND a module prefix.
395
+ # Equivalent to: scope path: "admin", module: "admin"
396
+ #
397
+ # Example:
398
+ # namespace :admin do
399
+ # resources :users # → /admin/users, controller: "admin/users"
400
+ # end
401
+ def namespace(name, options = {}, &)
402
+ segment = name.to_s
403
+ merged = { path: segment, module: segment }.merge(options)
404
+ scope(merged, &)
405
+ end
406
+
407
+ # Rails-like `scope` — groups routes with shared options (path prefix, module,
408
+ # auth, tables, controller).
409
+ #
410
+ # Examples:
411
+ # scope path: "admin" do
412
+ # resources :users # → /admin/users
413
+ # end
414
+ #
415
+ # scope module: "v2" do
416
+ # resources :users # → /users, controller: "v2/users"
417
+ # end
418
+ #
419
+ # scope path: "v1", module: "v1", auth: :cognito do
420
+ # resources :posts # → /v1/posts, controller: "v1/posts", auth: cognito
421
+ # end
310
422
  def scope(options = {}, &)
311
423
  previous_prefix = @scope_prefix
312
424
  previous_module = @scope_module
@@ -314,12 +426,16 @@ module Belt
314
426
  previous_tables = @scope_tables
315
427
  previous_controller = @scope_controller
316
428
 
317
- # Nest path segments (Rails-style): scope path: "a" { scope path: "b" } → "a/b"
429
+ # Nest path segments: scope path: "a" { scope path: "b" } → "a/b"
318
430
  if options.key?(:path)
319
431
  segment = options[:path].to_s.gsub(%r{^/|/$}, '')
320
432
  @scope_prefix = @scope_prefix.to_s.empty? ? segment : "#{@scope_prefix}/#{segment}"
321
433
  end
322
- @scope_module = options[:module] || @scope_module
434
+ # Nest module segments: namespace :admin { namespace :v2 } → "admin/v2"
435
+ if options.key?(:module)
436
+ mod_segment = options[:module].to_s
437
+ @scope_module = @scope_module.to_s.empty? ? mod_segment : "#{@scope_module}/#{mod_segment}"
438
+ end
323
439
  @scope_auth = options[:auth] || @scope_auth
324
440
  @scope_tables = (@scope_tables + Array(options[:tables] || [])).uniq
325
441
  @scope_controller = options[:controller] || @scope_controller
@@ -333,85 +449,93 @@ module Belt
333
449
  @scope_controller = previous_controller
334
450
  end
335
451
 
452
+ # Target a different Lambda function for enclosed routes.
453
+ # Routes within this block will have their :lambda field set to `name`.
454
+ # Does NOT affect path prefixes or controller module resolution.
455
+ #
456
+ # Example:
457
+ # gateway :api, auth: :cognito do
458
+ # resources :posts # → lambda: "api"
459
+ #
460
+ # function :onboarding do
461
+ # resources :stuff # → lambda: "onboarding"
462
+ # end
463
+ #
464
+ # function :custom do
465
+ # get '/blah' # → lambda: "custom"
466
+ # end
467
+ # end
468
+ def function(name, options = {}, &)
469
+ previous_lambda = @lambda_target
470
+ @lambda_target = name.to_s
471
+ # function blocks can also carry auth/tables
472
+ previous_auth = @scope_auth
473
+ previous_tables = @scope_tables
474
+ @scope_auth = options[:auth] if options[:auth]
475
+ @scope_tables = (@scope_tables + Array(options[:tables] || [])).uniq
476
+
477
+ instance_eval(&) if block_given?
478
+
479
+ @lambda_target = previous_lambda
480
+ @scope_auth = previous_auth
481
+ @scope_tables = previous_tables
482
+ end
483
+
336
484
  %i[get post put delete patch].each do |method|
337
485
  define_method(method) do |path, options = {}|
338
486
  full_path = build_path(path)
339
- route_options = options.dup
340
- route_options[:lambda] ||= @scope_module if @scope_module
341
- route_options[:auth] ||= @scope_auth if @scope_auth
342
- route_options[:controller] ||= @scope_controller if @scope_controller
343
- if @scope_tables.any? || route_options[:tables]
344
- route_options[:tables] =
345
- (@scope_tables + Array(route_options[:tables] || [])).uniq
346
- end
487
+ route_options = apply_scope_to_route(options)
347
488
  @gateway.send(method, full_path, route_options)
348
489
  end
349
490
  end
350
491
 
351
- def resources(name, options = {}, &block)
492
+ # Rails-like `root` — defines a GET / route.
493
+ #
494
+ # Examples:
495
+ # root to: "welcome#show"
496
+ # root action: :show, controller: :welcome
497
+ # root "pages#home"
498
+ def root(options_or_target = nil, **options)
499
+ if options_or_target.is_a?(String)
500
+ options[:to] = options_or_target
501
+ elsif options_or_target.is_a?(Hash)
502
+ options = options_or_target.merge(options)
503
+ end
504
+ options[:auth] ||= :none
505
+ route_options = apply_scope_to_route(options)
506
+ @gateway.send(:get, '/', route_options)
507
+ end
508
+
509
+ def resources(name, options = {}, &)
352
510
  options = apply_scope_options(options)
353
511
 
354
- if @scope_prefix.empty?
355
- @gateway.resources(name, options, &block)
356
- else
357
- # When inside a scope, generate routes with the prefix applied.
358
- # Also set the controller explicitly so inference resolves correctly
359
- # (e.g., scope "admin" + resources :users → controller "admin/users").
512
+ if @scope_prefix.empty? && @scope_module.nil?
513
+ @gateway.resources(name, options, &)
514
+ elsif @scope_prefix.empty?
360
515
  resource_name = name.to_s
361
- singular = Belt::Inflector.singularize(resource_name)
362
- param_name = options[:param] || "#{singular}_id"
363
- controller = "#{@scope_prefix}/#{resource_name}"
364
- resource_options = options.merge(route_type: :resources, controller: controller)
365
- actions = determine_scoped_actions(options)
366
-
367
- add_scoped_resource_routes(resource_name, param_name, resource_options, actions)
368
-
369
- if block
370
- collection_prefix = build_path("/#{resource_name}")
371
- member_prefix = build_path("/#{resource_name}/{#{param_name}}")
372
- resource_tables = Array(options[:tables] || [])
373
- inherited_tables = (@gateway.default_tables + resource_tables).uniq
374
- inherited_auth = options[:auth] || @gateway.default_auth
375
- nested_builder = NestedResourceBuilder.new(@gateway, member_prefix, collection_prefix,
376
- inherited_tables: inherited_tables,
377
- inherited_auth: inherited_auth,
378
- inherited_controller: controller)
379
- nested_builder.instance_eval(&block)
380
- end
516
+ controller = determine_scoped_controller(resource_name)
517
+ options = options.merge(controller: controller) unless options[:controller]
518
+ @gateway.resources(name, options, &)
519
+ else
520
+ build_scoped_resources(name, options, &)
381
521
  end
382
522
  end
383
523
 
384
524
  def resource(name, options = {})
385
525
  options = apply_scope_options(options)
386
526
 
387
- if @scope_prefix.empty?
527
+ if @scope_prefix.empty? && @scope_module.nil?
388
528
  @gateway.resource(name, options)
389
- else
529
+ elsif @scope_prefix.empty?
390
530
  resource_name = name.to_s
391
- controller = "#{@scope_prefix}/#{resource_name}"
392
- resource_options = options.merge(route_type: :resource, controller: controller)
393
- actions = determine_scoped_actions(options, default: %i[show update destroy create])
394
-
395
- @gateway.send(:add_route, :get, build_path("/#{resource_name}"), resource_options) if actions.include?(:show)
396
- if actions.include?(:update)
397
- @gateway.send(:add_route, :put, build_path("/#{resource_name}"),
398
- resource_options)
399
- end
400
- if actions.include?(:destroy)
401
- @gateway.send(:add_route, :delete, build_path("/#{resource_name}"),
402
- resource_options)
403
- end
404
- if actions.include?(:create)
405
- @gateway.send(:add_route, :post, build_path("/#{resource_name}"),
406
- resource_options)
407
- end
531
+ controller = determine_scoped_controller(resource_name)
532
+ options = options.merge(controller: controller) unless options[:controller]
533
+ @gateway.resource(name, options)
534
+ else
535
+ build_scoped_resource(name, options)
408
536
  end
409
537
  end
410
538
 
411
- def lambda(name, &)
412
- name
413
- end
414
-
415
539
  def mount(mountable, options = {})
416
540
  prefix = options[:at]&.to_s&.gsub(%r{^/|/$}, '') || ''
417
541
  extra_tables = Array(options[:tables] || [])
@@ -456,6 +580,16 @@ module Belt
456
580
  @scope_prefix.empty? ? path : "/#{@scope_prefix}#{path}"
457
581
  end
458
582
 
583
+ def determine_scoped_controller(resource_name)
584
+ if @scope_module && !@scope_module.empty?
585
+ "#{@scope_module}/#{resource_name}"
586
+ elsif !@scope_prefix.empty?
587
+ "#{@scope_prefix}/#{resource_name}"
588
+ else
589
+ resource_name
590
+ end
591
+ end
592
+
459
593
  def determine_scoped_actions(options, default: %i[index create show update destroy])
460
594
  if options[:only]
461
595
  Array(options[:only])
@@ -467,23 +601,107 @@ module Belt
467
601
  end
468
602
 
469
603
  def add_scoped_resource_routes(resource_name, param_name, resource_options, actions)
470
- @gateway.send(:add_route, :get, build_path("/#{resource_name}"), resource_options) if actions.include?(:index)
471
- @gateway.send(:add_route, :post, build_path("/#{resource_name}"), resource_options) if actions.include?(:create)
604
+ if actions.include?(:index)
605
+ @gateway.send(:add_route, :get, build_path("/#{resource_name}"),
606
+ resolve_request_model_for(resource_options, :index))
607
+ end
608
+ if actions.include?(:create)
609
+ @gateway.send(:add_route, :post, build_path("/#{resource_name}"),
610
+ resolve_request_model_for(resource_options, :create))
611
+ end
472
612
  if actions.include?(:show)
473
- @gateway.send(:add_route, :get, build_path("/#{resource_name}/{#{param_name}}"), resource_options)
613
+ @gateway.send(:add_route, :get, build_path("/#{resource_name}/{#{param_name}}"),
614
+ resolve_request_model_for(resource_options, :show))
474
615
  end
475
616
  if actions.include?(:update)
476
- @gateway.send(:add_route, :put, build_path("/#{resource_name}/{#{param_name}}"), resource_options)
617
+ @gateway.send(:add_route, :put, build_path("/#{resource_name}/{#{param_name}}"),
618
+ resolve_request_model_for(resource_options, :update))
477
619
  end
478
620
  return unless actions.include?(:destroy)
479
621
 
480
- @gateway.send(:add_route, :delete, build_path("/#{resource_name}/{#{param_name}}"), resource_options)
622
+ @gateway.send(:add_route, :delete, build_path("/#{resource_name}/{#{param_name}}"),
623
+ resolve_request_model_for(resource_options, :destroy))
624
+ end
625
+
626
+ def resolve_request_model_for(options, action)
627
+ rm = options[:request_model]
628
+ case rm
629
+ when Hash
630
+ resolved = rm[action] || rm[action.to_s]
631
+ options.merge(request_model: resolved)
632
+ else
633
+ options
634
+ end
635
+ end
636
+
637
+ def build_scoped_resources(name, options, &block)
638
+ resource_name = name.to_s
639
+ singular = Belt::Inflector.singularize(resource_name)
640
+ param_name = options[:param] || "#{singular}_id"
641
+ controller = options[:controller] || determine_scoped_controller(resource_name)
642
+ resource_options = options.merge(route_type: :resources, controller: controller)
643
+ actions = determine_scoped_actions(options)
644
+
645
+ add_scoped_resource_routes(resource_name, param_name, resource_options, actions)
646
+ build_nested_resource_block(resource_name, param_name, options, controller, &block) if block
647
+ end
648
+
649
+ def build_nested_resource_block(resource_name, param_name, options, controller, &)
650
+ collection_prefix = build_path("/#{resource_name}")
651
+ member_prefix = build_path("/#{resource_name}/{#{param_name}}")
652
+ resource_tables = Array(options[:tables] || [])
653
+ inherited_tables = (@gateway.default_tables + resource_tables).uniq
654
+ inherited_auth = options[:auth] || @gateway.default_auth
655
+ nested_builder = NestedResourceBuilder.new(@gateway, member_prefix, collection_prefix,
656
+ inherited_tables: inherited_tables,
657
+ inherited_auth: inherited_auth,
658
+ inherited_controller: controller,
659
+ inherited_lambda: @lambda_target)
660
+ nested_builder.instance_eval(&)
661
+ end
662
+
663
+ def build_scoped_resource(name, options)
664
+ resource_name = name.to_s
665
+ controller = options[:controller] || determine_scoped_controller(resource_name)
666
+ resource_options = options.merge(route_type: :resource, controller: controller)
667
+ actions = determine_scoped_actions(options, default: %i[show update destroy create])
668
+
669
+ if actions.include?(:show)
670
+ @gateway.send(:add_route, :get, build_path("/#{resource_name}"),
671
+ resolve_request_model_for(resource_options, :show))
672
+ end
673
+ if actions.include?(:update)
674
+ @gateway.send(:add_route, :put, build_path("/#{resource_name}"),
675
+ resolve_request_model_for(resource_options, :update))
676
+ end
677
+ if actions.include?(:destroy)
678
+ @gateway.send(:add_route, :delete, build_path("/#{resource_name}"),
679
+ resolve_request_model_for(resource_options, :destroy))
680
+ end
681
+ return unless actions.include?(:create)
682
+
683
+ @gateway.send(:add_route, :post, build_path("/#{resource_name}"),
684
+ resolve_request_model_for(resource_options, :create))
685
+ end
686
+
687
+ def apply_scope_to_route(options)
688
+ route_options = options.dup
689
+ # Lambda target: only explicit function block affects it
690
+ route_options[:lambda] ||= @lambda_target if @lambda_target
691
+ route_options[:auth] ||= @scope_auth if @scope_auth
692
+ route_options[:controller] ||= @scope_controller if @scope_controller
693
+ if @scope_tables.any? || route_options[:tables]
694
+ route_options[:tables] =
695
+ (@scope_tables + Array(route_options[:tables] || [])).uniq
696
+ end
697
+ route_options
481
698
  end
482
699
 
483
700
  def apply_scope_options(options)
484
701
  result = options.dup
485
702
  result[:auth] ||= @scope_auth if @scope_auth
486
- result[:lambda] ||= @scope_module if @scope_module
703
+ # Lambda target: only explicit function block affects it
704
+ result[:lambda] ||= @lambda_target if @lambda_target
487
705
  result[:controller] ||= @scope_controller if @scope_controller
488
706
  result[:tables] = (@scope_tables + Array(result[:tables] || [])).uniq if @scope_tables.any? || result[:tables]
489
707
  result
data/lib/belt/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Belt
4
- VERSION = '0.2.17'
4
+ VERSION = '0.3.15'
5
5
  end
@@ -1,6 +1,6 @@
1
1
  # Declarative frontend env map for belt.
2
2
  # Left: env var your build tool reads. Right: terraform output name.
3
- # Copy to frontend/env.yml (or .belt/frontend_env.yml) and adjust.
3
+ # Copy to <frontend>/env.yml (or .belt/frontend_env.yml for the default frontend/) and adjust.
4
4
  #
5
5
  # belt deploy frontend <env> → injects these into npm run build
6
6
  # belt frontend env <env> → smart-merges into frontend/.env for local dev
@@ -9,5 +9,5 @@
9
9
 
10
10
  VITE_API_URL: api_url
11
11
  # VITE_COGNITO_USER_POOL_ID: cognito_user_pool_id
12
- # VITE_COGNITO_CLIENT_ID: cognito_user_pool_client_id
12
+ # VITE_COGNITO_CLIENT_ID: cognito_client_id
13
13
  # VITE_AWS_REGION: cognito_region