belt 0.3.4 → 0.3.6

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.

Potentially problematic release.


This version of belt might be problematic. Click here for more details.

@@ -0,0 +1,103 @@
1
+ # Frontends
2
+
3
+ Belt can host one or more JavaScript SPAs (React, Vue, or Svelte) next to the
4
+ Lambda API. A typical app has a single `frontend/` directory. Some apps — Stowzilla
5
+ is the example that drove this — have several independent SPAs (customer, ops,
6
+ partners) that share the same models and API.
7
+
8
+ ## Single frontend (default)
9
+
10
+ ```bash
11
+ belt new blog --frontend react
12
+ # or, in an existing app:
13
+ belt generate frontend react
14
+ ```
15
+
16
+ That creates `frontend/`, S3 + CloudFront infrastructure, and wires CORS so the
17
+ SPA can call the API. No config file is required.
18
+
19
+ ```bash
20
+ belt server # local Vite dev server
21
+ belt frontend env dev01 # write frontend/.env from terraform outputs
22
+ belt deploy frontend dev01 # npm ci → build → S3 sync → CloudFront invalidation
23
+ ```
24
+
25
+ ## Multiple frontends
26
+
27
+ Declare them in `config/frontends.yml` (or `.belt/frontends.yml`):
28
+
29
+ ```yaml
30
+ frontends:
31
+ customer:
32
+ path: app
33
+ dist: build
34
+ default: true
35
+ bucket_output: web_app_bucket_name
36
+ url_output: web_app_url
37
+ cloudfront_domain_output: web_app_cloudfront_domain
38
+ ops:
39
+ path: ops-app
40
+ dist: build
41
+ bucket_output: ops_app_bucket_name
42
+ url_output: ops_app_url
43
+ cloudfront_domain_output: ops_app_cloudfront_domain
44
+ ```
45
+
46
+ `path` is the directory. `dist` is the build output (`dist` by default; belt
47
+ also auto-detects `build/` after `npm run build`). Terraform output names default
48
+ to `frontend_*` for the `frontend` app and `{name}_frontend_*` for others — override
49
+ them when existing infrastructure uses different names.
50
+
51
+ If terraform exports a CloudFront **domain** instead of a distribution ID, set
52
+ `cloudfront_domain_output` and skip `distribution_output`. Belt looks the ID up
53
+ via AWS and will not probe `{name}_frontend_distribution_id`.
54
+
55
+ ```bash
56
+ belt frontend list
57
+ belt generate frontend react --name ops --path ops-app
58
+ belt generate views bag --frontend ops
59
+ belt generate scaffold order --frontend customer
60
+ belt server --frontend ops
61
+ belt frontend env dev01 --frontend customer
62
+ belt deploy frontend dev01 # all configured frontends
63
+ belt deploy frontend dev01 --frontend ops # just ops
64
+ ```
65
+
66
+ If several frontends exist and you omit `--frontend`, generators and `belt server`
67
+ use the one marked `default: true`. If none is default, they ask you to pick.
68
+
69
+ `belt deploy frontend <env>` with no `--frontend` deploys every frontend that
70
+ has a `package.json`. Full `belt deploy <env>` does the same after terraform apply.
71
+
72
+ ## Env maps
73
+
74
+ Each frontend can have its own `env.yml` mapping process env names to terraform
75
+ outputs:
76
+
77
+ ```yaml
78
+ # app/env.yml
79
+ VITE_API_URL: api_url
80
+ VITE_COGNITO_USER_POOL_ID: cognito_user_pool_id
81
+ VITE_COGNITO_CLIENT_ID: cognito_user_pool_client_id
82
+ ```
83
+
84
+ The default `frontend/` directory also accepts `.belt/frontend_env.yml` as a
85
+ fallback. See the existing env-map behavior: only mapped keys are written into
86
+ `.env`; missing terraform outputs warn and do not clobber local values.
87
+
88
+ ## Infrastructure
89
+
90
+ `belt setup frontend` (and `belt generate frontend`) writes S3 + CloudFront into
91
+ `infrastructure/modules/app/frontend.tf`. Additional named frontends get
92
+ `{name}_frontend.tf` with unique resource names and outputs
93
+ (`ops_frontend_bucket_name`, …). Extra frontends get a CloudFront URL only —
94
+ custom DNS stays on the default frontend unless you add records yourself.
95
+
96
+ CORS: each CloudFront domain is added to `frontend_urls` on the conveyor-belt
97
+ resource so SPA → API calls work.
98
+
99
+ ## See Also
100
+
101
+ - `belt explain deployment` — how frontend deploy fits into `belt deploy`
102
+ - `belt explain generators` — `belt generate frontend` / views
103
+ - `belt explain structure` — where frontend directories live
@@ -51,8 +51,12 @@ Adds a frontend framework to the project:
51
51
  belt generate frontend react
52
52
  belt generate frontend vue
53
53
  belt generate frontend svelte
54
+ belt generate frontend react --name ops --path ops-app
54
55
  ```
55
56
 
57
+ `--name` and `--path` scaffold an additional SPA and register it in
58
+ `config/frontends.yml`. See `belt explain frontend`.
59
+
56
60
  ### Views
57
61
 
58
62
  Generates React pages for a resource's REST actions:
@@ -60,8 +64,12 @@ Generates React pages for a resource's REST actions:
60
64
  ```bash
61
65
  belt generate views post title:string body:text status:string
62
66
  belt g views comment body:text author:string
67
+ belt g views bag --frontend ops
63
68
  ```
64
69
 
70
+ When the app has multiple frontends, pass `--frontend <name>` (or mark one
71
+ `default: true` in `config/frontends.yml`).
72
+
65
73
  ### Auth
66
74
 
67
75
  Sets up Cognito authentication:
@@ -94,6 +102,7 @@ belt destroy model comment
94
102
  belt destroy controller admin/users
95
103
  belt destroy environment staging
96
104
  belt destroy frontend
105
+ belt destroy frontend --frontend ops
97
106
  belt destroy views post
98
107
  ```
99
108
 
@@ -43,6 +43,7 @@ my-app/
43
43
  │ └── prod/
44
44
  │ └── ...
45
45
  ├── frontend/ # Optional frontend (React/Vue/Svelte)
46
+ ├── config/frontends.yml # Optional: multiple named frontends
46
47
  ├── Gemfile # Project-level dependencies (CLI, dev tools)
47
48
  ├── Rakefile # Rake tasks
48
49
  ├── AGENTS.md # AI agent guide
@@ -85,6 +86,8 @@ Terraform resources.
85
86
  `lambda/Gemfile` is what gets packaged into the Lambda.
86
87
  - **Config over code**: Lambda configuration (timeout, memory, env vars) goes in
87
88
  YAML files, not hardcoded in Terraform.
89
+ - **Frontends**: Default directory is `frontend/`. Multiple SPAs (customer + ops,
90
+ etc.) are declared in `config/frontends.yml`. See `belt explain frontend`.
88
91
 
89
92
  ## See Also
90
93
 
@@ -106,17 +106,37 @@ module Belt
106
106
  private
107
107
 
108
108
  def add_nested_resource_routes(resource_name, param_name, resource_options, actions)
109
- @gateway.send(:add_route, :get, "#{@prefix}/#{resource_name}", resource_options) if actions.include?(:index)
110
- @gateway.send(:add_route, :post, "#{@prefix}/#{resource_name}", resource_options) if actions.include?(:create)
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
111
117
  if actions.include?(:show)
112
- @gateway.send(:add_route, :get, "#{@prefix}/#{resource_name}/{#{param_name}}", resource_options)
118
+ @gateway.send(:add_route, :get, "#{@prefix}/#{resource_name}/{#{param_name}}",
119
+ resolve_request_model_for(resource_options, :show))
113
120
  end
114
121
  if actions.include?(:update)
115
- @gateway.send(:add_route, :put, "#{@prefix}/#{resource_name}/{#{param_name}}", resource_options)
122
+ @gateway.send(:add_route, :put, "#{@prefix}/#{resource_name}/{#{param_name}}",
123
+ resolve_request_model_for(resource_options, :update))
116
124
  end
117
125
  return unless actions.include?(:destroy)
118
126
 
119
- @gateway.send(:add_route, :delete, "#{@prefix}/#{resource_name}/{#{param_name}}", resource_options)
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
120
140
  end
121
141
 
122
142
  def merge_inherited_options(options)
@@ -200,11 +220,7 @@ module Belt
200
220
  resource_options = options.merge(route_type: :resources)
201
221
  actions = determine_actions(options)
202
222
 
203
- add_route(:get, "/#{resource_name}", resource_options) if actions.include?(:index)
204
- add_route(:post, "/#{resource_name}", resource_options) if actions.include?(:create)
205
- add_route(:get, "/#{resource_name}/{#{param_name}}", resource_options) if actions.include?(:show)
206
- add_route(:put, "/#{resource_name}/{#{param_name}}", resource_options) if actions.include?(:update)
207
- add_route(:delete, "/#{resource_name}/{#{param_name}}", resource_options) if actions.include?(:destroy)
223
+ add_resource_routes(resource_name, param_name, resource_options, actions)
208
224
 
209
225
  return unless block_given?
210
226
 
@@ -226,10 +242,22 @@ module Belt
226
242
  actions = determine_actions(options, default: %i[show update destroy])
227
243
  resource_options = options.merge(route_type: :resource)
228
244
 
229
- add_route(:get, "/#{resource_name}", resource_options) if actions.include?(:show)
230
- add_route(:put, "/#{resource_name}", resource_options) if actions.include?(:update)
231
- add_route(:delete, "/#{resource_name}", resource_options) if actions.include?(:destroy)
232
- 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))
233
261
  end
234
262
 
235
263
  private
@@ -271,6 +299,39 @@ module Belt
271
299
  options.merge(tables: [resource_name.to_sym])
272
300
  end
273
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
+
274
335
  def determine_actions(options, default: %i[index create show update destroy])
275
336
  if options[:only]
276
337
  Array(options[:only])
@@ -540,17 +601,37 @@ module Belt
540
601
  end
541
602
 
542
603
  def add_scoped_resource_routes(resource_name, param_name, resource_options, actions)
543
- @gateway.send(:add_route, :get, build_path("/#{resource_name}"), resource_options) if actions.include?(:index)
544
- @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
545
612
  if actions.include?(:show)
546
- @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))
547
615
  end
548
616
  if actions.include?(:update)
549
- @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))
550
619
  end
551
620
  return unless actions.include?(:destroy)
552
621
 
553
- @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
554
635
  end
555
636
 
556
637
  def build_scoped_resources(name, options, &block)
@@ -585,13 +666,22 @@ module Belt
585
666
  resource_options = options.merge(route_type: :resource, controller: controller)
586
667
  actions = determine_scoped_actions(options, default: %i[show update destroy create])
587
668
 
588
- @gateway.send(:add_route, :get, build_path("/#{resource_name}"), resource_options) if actions.include?(:show)
589
- @gateway.send(:add_route, :put, build_path("/#{resource_name}"), resource_options) if actions.include?(:update)
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
590
677
  if actions.include?(:destroy)
591
678
  @gateway.send(:add_route, :delete, build_path("/#{resource_name}"),
592
- resource_options)
679
+ resolve_request_model_for(resource_options, :destroy))
593
680
  end
594
- @gateway.send(:add_route, :post, build_path("/#{resource_name}"), resource_options) if actions.include?(:create)
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))
595
685
  end
596
686
 
597
687
  def apply_scope_to_route(options)
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.3.4'
4
+ VERSION = '0.3.6'
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
@@ -1,15 +1,17 @@
1
- # Frontend hosting — S3 + CloudFront
1
+ # Frontend hosting — S3 + CloudFront<% if @include_dns %>
2
2
  # Custom domain attached when var.domain is set
3
-
4
- resource "random_string" "frontend_suffix" {
3
+ <% else %>
4
+ # Additional frontend '<%= @frontend.name %>' — CloudFront default domain (no Route53)
5
+ <% end %>
6
+ resource "random_string" "<%= @tf_name %>_suffix" {
5
7
  length = 8
6
8
  special = false
7
9
  upper = false
8
10
  }
9
11
 
10
12
  # S3 bucket for frontend static assets
11
- resource "aws_s3_bucket" "frontend" {
12
- bucket = "${var.app_name}-frontend-${var.environment}-${random_string.frontend_suffix.result}"
13
+ resource "aws_s3_bucket" "<%= @tf_name %>" {
14
+ bucket = "${var.app_name}-<%= @bucket_slug %>-${var.environment}-${random_string.<%= @tf_name %>_suffix.result}"
13
15
  force_destroy = true
14
16
 
15
17
  lifecycle {
@@ -17,8 +19,8 @@ resource "aws_s3_bucket" "frontend" {
17
19
  }
18
20
  }
19
21
 
20
- resource "aws_s3_bucket_public_access_block" "frontend" {
21
- bucket = aws_s3_bucket.frontend.id
22
+ resource "aws_s3_bucket_public_access_block" "<%= @tf_name %>" {
23
+ bucket = aws_s3_bucket.<%= @tf_name %>.id
22
24
 
23
25
  block_public_acls = true
24
26
  block_public_policy = true
@@ -26,8 +28,8 @@ resource "aws_s3_bucket_public_access_block" "frontend" {
26
28
  restrict_public_buckets = true
27
29
  }
28
30
 
29
- resource "aws_s3_bucket_server_side_encryption_configuration" "frontend" {
30
- bucket = aws_s3_bucket.frontend.id
31
+ resource "aws_s3_bucket_server_side_encryption_configuration" "<%= @tf_name %>" {
32
+ bucket = aws_s3_bucket.<%= @tf_name %>.id
31
33
 
32
34
  rule {
33
35
  apply_server_side_encryption_by_default {
@@ -38,36 +40,36 @@ resource "aws_s3_bucket_server_side_encryption_configuration" "frontend" {
38
40
  }
39
41
 
40
42
  # CloudFront OAC
41
- resource "aws_cloudfront_origin_access_control" "frontend" {
42
- name = "${var.app_name}-${var.environment}-frontend-oac"
43
- description = "OAC for frontend bucket"
43
+ resource "aws_cloudfront_origin_access_control" "<%= @tf_name %>" {
44
+ name = "${var.app_name}-${var.environment}-<%= @bucket_slug %>-oac"
45
+ description = "OAC for <%= @bucket_slug %> bucket"
44
46
  origin_access_control_origin_type = "s3"
45
47
  signing_behavior = "always"
46
48
  signing_protocol = "sigv4"
47
49
  }
48
50
 
49
51
  # CloudFront distribution
50
- resource "aws_cloudfront_distribution" "frontend" {
52
+ resource "aws_cloudfront_distribution" "<%= @tf_name %>" {
51
53
  origin {
52
- domain_name = aws_s3_bucket.frontend.bucket_regional_domain_name
53
- origin_id = "S3-${aws_s3_bucket.frontend.id}"
54
- origin_access_control_id = aws_cloudfront_origin_access_control.frontend.id
54
+ domain_name = aws_s3_bucket.<%= @tf_name %>.bucket_regional_domain_name
55
+ origin_id = "S3-${aws_s3_bucket.<%= @tf_name %>.id}"
56
+ origin_access_control_id = aws_cloudfront_origin_access_control.<%= @tf_name %>.id
55
57
  }
56
58
 
57
59
  enabled = true
58
60
  is_ipv6_enabled = true
59
61
  default_root_object = "index.html"
60
-
62
+ <% if @include_dns %>
61
63
  # Custom domain aliases (if DNS configured)
62
64
  aliases = local.dns_enabled ? compact([
63
65
  local.app_domain,
64
66
  var.environment == "prod" ? "www.${var.domain}" : ""
65
67
  ]) : []
66
-
68
+ <% end %>
67
69
  default_cache_behavior {
68
70
  allowed_methods = ["GET", "HEAD", "OPTIONS"]
69
71
  cached_methods = ["GET", "HEAD"]
70
- target_origin_id = "S3-${aws_s3_bucket.frontend.id}"
72
+ target_origin_id = "S3-${aws_s3_bucket.<%= @tf_name %>.id}"
71
73
  compress = true
72
74
  viewer_protocol_policy = "redirect-to-https"
73
75
 
@@ -99,22 +101,22 @@ resource "aws_cloudfront_distribution" "frontend" {
99
101
  }
100
102
 
101
103
  viewer_certificate {
102
- cloudfront_default_certificate = !local.dns_enabled
103
- acm_certificate_arn = local.dns_enabled ? aws_acm_certificate_validation.app[0].certificate_arn : null
104
- ssl_support_method = local.dns_enabled ? "sni-only" : null
105
- minimum_protocol_version = local.dns_enabled ? "TLSv1.2_2021" : null
104
+ cloudfront_default_certificate = <%= @include_dns ? '!local.dns_enabled' : 'true' %>
105
+ acm_certificate_arn = <%= @include_dns ? 'local.dns_enabled ? aws_acm_certificate_validation.app[0].certificate_arn : null' : 'null' %>
106
+ ssl_support_method = <%= @include_dns ? 'local.dns_enabled ? "sni-only" : null' : 'null' %>
107
+ minimum_protocol_version = <%= @include_dns ? 'local.dns_enabled ? "TLSv1.2_2021" : null' : 'null' %>
106
108
  }
107
109
 
108
110
  tags = {
109
- Name = "${var.app_name}-${var.environment}-frontend"
111
+ Name = "${var.app_name}-${var.environment}-<%= @bucket_slug %>"
110
112
  Environment = var.environment
111
113
  ManagedBy = "Terraform"
112
114
  }
113
115
  }
114
116
 
115
117
  # S3 bucket policy — CloudFront OAC access only
116
- resource "aws_s3_bucket_policy" "frontend" {
117
- bucket = aws_s3_bucket.frontend.id
118
+ resource "aws_s3_bucket_policy" "<%= @tf_name %>" {
119
+ bucket = aws_s3_bucket.<%= @tf_name %>.id
118
120
 
119
121
  policy = jsonencode({
120
122
  Version = "2012-10-17"
@@ -126,55 +128,59 @@ resource "aws_s3_bucket_policy" "frontend" {
126
128
  Service = "cloudfront.amazonaws.com"
127
129
  }
128
130
  Action = "s3:GetObject"
129
- Resource = "${aws_s3_bucket.frontend.arn}/*"
131
+ Resource = "${aws_s3_bucket.<%= @tf_name %>.arn}/*"
130
132
  Condition = {
131
133
  StringEquals = {
132
- "AWS:SourceArn" = aws_cloudfront_distribution.frontend.arn
134
+ "AWS:SourceArn" = aws_cloudfront_distribution.<%= @tf_name %>.arn
133
135
  }
134
136
  }
135
137
  }
136
138
  ]
137
139
  })
138
140
 
139
- depends_on = [aws_s3_bucket_public_access_block.frontend]
141
+ depends_on = [aws_s3_bucket_public_access_block.<%= @tf_name %>]
140
142
  }
141
-
143
+ <% if @include_dns %>
142
144
  # Route53 record for frontend (app domain → CloudFront)
143
- resource "aws_route53_record" "frontend" {
145
+ resource "aws_route53_record" "<%= @tf_name %>" {
144
146
  count = local.dns_enabled ? 1 : 0
145
147
  zone_id = aws_route53_zone.app[0].zone_id
146
148
  name = local.app_domain
147
149
  type = "A"
148
150
 
149
151
  alias {
150
- name = aws_cloudfront_distribution.frontend.domain_name
151
- zone_id = aws_cloudfront_distribution.frontend.hosted_zone_id
152
+ name = aws_cloudfront_distribution.<%= @tf_name %>.domain_name
153
+ zone_id = aws_cloudfront_distribution.<%= @tf_name %>.hosted_zone_id
152
154
  evaluate_target_health = false
153
155
  }
154
156
  }
155
157
 
156
158
  # www record for prod only
157
- resource "aws_route53_record" "frontend_www" {
159
+ resource "aws_route53_record" "<%= @tf_name %>_www" {
158
160
  count = local.dns_enabled && var.environment == "prod" ? 1 : 0
159
161
  zone_id = aws_route53_zone.app[0].zone_id
160
162
  name = "www.${var.domain}"
161
163
  type = "A"
162
164
 
163
165
  alias {
164
- name = aws_cloudfront_distribution.frontend.domain_name
165
- zone_id = aws_cloudfront_distribution.frontend.hosted_zone_id
166
+ name = aws_cloudfront_distribution.<%= @tf_name %>.domain_name
167
+ zone_id = aws_cloudfront_distribution.<%= @tf_name %>.hosted_zone_id
166
168
  evaluate_target_health = false
167
169
  }
168
170
  }
169
-
170
- output "frontend_bucket_name" {
171
- value = aws_s3_bucket.frontend.id
171
+ <% end %>
172
+ output "<%= @output_prefix %>_bucket_name" {
173
+ value = aws_s3_bucket.<%= @tf_name %>.id
172
174
  }
173
175
 
174
- output "frontend_distribution_id" {
175
- value = aws_cloudfront_distribution.frontend.id
176
+ output "<%= @output_prefix %>_distribution_id" {
177
+ value = aws_cloudfront_distribution.<%= @tf_name %>.id
176
178
  }
177
179
 
178
- output "frontend_url" {
179
- value = local.dns_enabled ? "https://${local.app_domain}" : "https://${aws_cloudfront_distribution.frontend.domain_name}"
180
+ output "<%= @output_prefix %>_url" {
181
+ <% if @include_dns -%>
182
+ value = local.dns_enabled ? "https://${local.app_domain}" : "https://${aws_cloudfront_distribution.<%= @tf_name %>.domain_name}"
183
+ <% else -%>
184
+ value = "https://${aws_cloudfront_distribution.<%= @tf_name %>.domain_name}"
185
+ <% end -%>
180
186
  }
@@ -161,6 +161,35 @@ belt doctor # Check dependencies
161
161
 
162
162
  4. `resources :things` generates: `GET /things`, `POST /things`, `GET /things/:id`, `PUT /things/:id`, `DELETE /things/:id`. **PUT, not PATCH.**
163
163
 
164
+ ### Request Model Wiring (Validation)
165
+
166
+ Belt auto-wires request validation from `config/contracts.rb` using conventions:
167
+
168
+ - `POST /items` → looks for `:create_item` (or `:create_<gateway>_item`)
169
+ - `PUT /items/:id` → looks for `:update_item` (or `:update_<gateway>_item`)
170
+
171
+ **No `request_model:` declaration needed** if your contract names follow this pattern. Belt infers it.
172
+
173
+ ### Response Model Wiring (Documentation)
174
+
175
+ Belt auto-wires response models the same way:
176
+
177
+ - `resources :items` → looks for `model :item` in contracts → auto-wires `response_model: :item`
178
+
179
+ **No `response_model:` declaration needed** if your model name matches the singular resource name.
180
+
181
+ ### Overrides
182
+
183
+ For per-action overrides when conventions don't fit:
184
+ ```ruby
185
+ resources :items, request_model: { create: :create_item, update: :update_item }
186
+ ```
187
+
188
+ For non-CRUD custom actions, use explicit wiring:
189
+ ```ruby
190
+ post '/schedule', request_model: :schedule_pickup
191
+ ```
192
+
164
193
  ## How Models Work
165
194
 
166
195
  Models use ActiveItem (DynamoDB ORM):
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: belt
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.4
4
+ version: 0.3.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stowzilla
@@ -121,6 +121,7 @@ files:
121
121
  - lib/belt/cli/frontend_deploy_command.rb
122
122
  - lib/belt/cli/frontend_env_command.rb
123
123
  - lib/belt/cli/frontend_env_map.rb
124
+ - lib/belt/cli/frontend_registry.rb
124
125
  - lib/belt/cli/frontend_setup_command.rb
125
126
  - lib/belt/cli/generate_command.rb
126
127
  - lib/belt/cli/generator_registry.rb
@@ -131,6 +132,7 @@ files:
131
132
  - lib/belt/cli/path_gem_materializer.rb
132
133
  - lib/belt/cli/plugin_command.rb
133
134
  - lib/belt/cli/routes_command.rb
135
+ - lib/belt/cli/routes_command/request_model_inference.rb
134
136
  - lib/belt/cli/routes_command/route_inference.rb
135
137
  - lib/belt/cli/routes_command/schema_loader.rb
136
138
  - lib/belt/cli/server_command.rb
@@ -139,12 +141,14 @@ files:
139
141
  - lib/belt/cli/tasks_command.rb
140
142
  - lib/belt/cli/terraform_command.rb
141
143
  - lib/belt/cli/views_command.rb
144
+ - lib/belt/cli/zip_artifact_builder.rb
142
145
  - lib/belt/configuration.rb
143
146
  - lib/belt/controllers/welcome_controller.rb
144
147
  - lib/belt/docs/backups.md
145
148
  - lib/belt/docs/console.md
146
149
  - lib/belt/docs/controllers.md
147
150
  - lib/belt/docs/deployment.md
151
+ - lib/belt/docs/frontend.md
148
152
  - lib/belt/docs/generators.md
149
153
  - lib/belt/docs/lambda_handler.md
150
154
  - lib/belt/docs/models.md