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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +52 -0
- data/lib/belt/cli/auth_command.rb +27 -12
- data/lib/belt/cli/deploy_command.rb +27 -14
- data/lib/belt/cli/destroy_command.rb +55 -24
- data/lib/belt/cli/environment_command.rb +12 -0
- data/lib/belt/cli/explain_command.rb +3 -1
- data/lib/belt/cli/frontend_command.rb +31 -10
- data/lib/belt/cli/frontend_deploy_command.rb +110 -21
- data/lib/belt/cli/frontend_env_command.rb +55 -18
- data/lib/belt/cli/frontend_env_map.rb +27 -9
- data/lib/belt/cli/frontend_registry.rb +373 -0
- data/lib/belt/cli/frontend_setup_command.rb +71 -5
- data/lib/belt/cli/generate_command.rb +21 -6
- data/lib/belt/cli/routes_command/request_model_inference.rb +131 -0
- data/lib/belt/cli/routes_command.rb +7 -0
- data/lib/belt/cli/server_command.rb +28 -15
- data/lib/belt/cli/terraform_command.rb +9 -0
- data/lib/belt/cli/views_command.rb +13 -7
- data/lib/belt/cli/zip_artifact_builder.rb +199 -0
- data/lib/belt/cli.rb +12 -8
- data/lib/belt/docs/deployment.md +27 -2
- data/lib/belt/docs/frontend.md +103 -0
- data/lib/belt/docs/generators.md +9 -0
- data/lib/belt/docs/structure.md +3 -0
- data/lib/belt/route_dsl.rb +113 -23
- data/lib/belt/version.rb +1 -1
- data/lib/templates/frontend/react/env.yml.example +1 -1
- data/lib/templates/module/frontend.tf.erb +49 -43
- data/lib/templates/new_app/AGENTS.md.erb +29 -0
- metadata +5 -1
|
@@ -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
|
data/lib/belt/docs/generators.md
CHANGED
|
@@ -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
|
|
data/lib/belt/docs/structure.md
CHANGED
|
@@ -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
|
|
data/lib/belt/route_dsl.rb
CHANGED
|
@@ -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
|
-
|
|
110
|
-
|
|
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}}",
|
|
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}}",
|
|
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}}",
|
|
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
|
-
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
544
|
-
|
|
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}}"),
|
|
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}}"),
|
|
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}}"),
|
|
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
|
-
|
|
589
|
-
|
|
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
|
-
|
|
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,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
|
|
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
|
-
|
|
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" "
|
|
12
|
-
bucket = "${var.app_name}
|
|
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" "
|
|
21
|
-
bucket = aws_s3_bucket
|
|
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" "
|
|
30
|
-
bucket = aws_s3_bucket
|
|
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" "
|
|
42
|
-
name = "${var.app_name}-${var.environment}
|
|
43
|
-
description = "OAC for
|
|
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" "
|
|
52
|
+
resource "aws_cloudfront_distribution" "<%= @tf_name %>" {
|
|
51
53
|
origin {
|
|
52
|
-
domain_name = aws_s3_bucket
|
|
53
|
-
origin_id = "S3-${aws_s3_bucket
|
|
54
|
-
origin_access_control_id = aws_cloudfront_origin_access_control
|
|
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
|
|
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}
|
|
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" "
|
|
117
|
-
bucket = aws_s3_bucket
|
|
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
|
|
131
|
+
Resource = "${aws_s3_bucket.<%= @tf_name %>.arn}/*"
|
|
130
132
|
Condition = {
|
|
131
133
|
StringEquals = {
|
|
132
|
-
"AWS:SourceArn" = aws_cloudfront_distribution
|
|
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
|
|
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" "
|
|
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
|
|
151
|
-
zone_id = aws_cloudfront_distribution
|
|
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" "
|
|
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
|
|
165
|
-
zone_id = aws_cloudfront_distribution
|
|
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 "
|
|
171
|
-
value = aws_s3_bucket
|
|
171
|
+
<% end %>
|
|
172
|
+
output "<%= @output_prefix %>_bucket_name" {
|
|
173
|
+
value = aws_s3_bucket.<%= @tf_name %>.id
|
|
172
174
|
}
|
|
173
175
|
|
|
174
|
-
output "
|
|
175
|
-
value = aws_cloudfront_distribution
|
|
176
|
+
output "<%= @output_prefix %>_distribution_id" {
|
|
177
|
+
value = aws_cloudfront_distribution.<%= @tf_name %>.id
|
|
176
178
|
}
|
|
177
179
|
|
|
178
|
-
output "
|
|
179
|
-
|
|
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
|
+
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
|