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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +241 -0
- data/README.md +42 -3
- data/lib/belt/action_router.rb +34 -19
- data/lib/belt/cli/app_detection.rb +30 -3
- data/lib/belt/cli/auth_command.rb +395 -0
- data/lib/belt/cli/console_command.rb +17 -2
- data/lib/belt/cli/deploy_command.rb +51 -17
- data/lib/belt/cli/destroy_command.rb +62 -25
- data/lib/belt/cli/doctor_command.rb +166 -22
- data/lib/belt/cli/environment_command.rb +12 -0
- data/lib/belt/cli/explain_command.rb +112 -0
- 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 +56 -19
- data/lib/belt/cli/frontend_env_map.rb +30 -10
- 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 +61 -42
- data/lib/belt/cli/index_command.rb +192 -0
- data/lib/belt/cli/lambda_config_command.rb +9 -3
- data/lib/belt/cli/routes_command/request_model_inference.rb +131 -0
- data/lib/belt/cli/routes_command/route_inference.rb +4 -3
- data/lib/belt/cli/routes_command.rb +31 -19
- data/lib/belt/cli/server_command.rb +28 -15
- data/lib/belt/cli/tables_command.rb +3 -1
- data/lib/belt/cli/terraform_command.rb +9 -0
- data/lib/belt/cli/views_command.rb +15 -15
- data/lib/belt/cli/zip_artifact_builder.rb +199 -0
- data/lib/belt/cli.rb +17 -9
- data/lib/belt/docs/backups.md +101 -0
- data/lib/belt/docs/console.md +65 -0
- data/lib/belt/docs/controllers.md +155 -0
- data/lib/belt/docs/deployment.md +166 -0
- data/lib/belt/docs/frontend.md +103 -0
- data/lib/belt/docs/generators.md +137 -0
- data/lib/belt/docs/lambda_handler.md +105 -0
- data/lib/belt/docs/models.md +160 -0
- data/lib/belt/docs/observability.md +94 -0
- data/lib/belt/docs/plugins.md +94 -0
- data/lib/belt/docs/routing.md +138 -0
- data/lib/belt/docs/structure.md +96 -0
- data/lib/belt/inflector.rb +8 -0
- data/lib/belt/route_dsl.rb +318 -100
- data/lib/belt/version.rb +1 -1
- data/lib/templates/frontend/react/env.yml.example +2 -2
- data/lib/templates/generate/auth/cognito.tf.erb +71 -0
- data/lib/templates/generate/auth/cognito_outputs.tf.erb +19 -0
- data/lib/templates/generate/auth/frontend/ConfirmEmail.jsx +55 -0
- data/lib/templates/generate/auth/frontend/Login.jsx +79 -0
- data/lib/templates/generate/auth/frontend/ProtectedRoute.jsx +9 -0
- data/lib/templates/generate/auth/frontend/SignUp.jsx +58 -0
- data/lib/templates/generate/auth/frontend/apiClient.js +34 -0
- data/lib/templates/generate/auth/frontend/auth.css +157 -0
- data/lib/templates/generate/auth/frontend/auth.js +134 -0
- data/lib/templates/generate/model.rb.erb +4 -6
- data/lib/templates/module/frontend.tf.erb +49 -43
- data/lib/templates/module/outputs.tf.erb +1 -1
- data/lib/templates/new_app/AGENTS.md.erb +170 -20
- data/lib/templates/new_app/config/lambda/api.yml.erb +1 -0
- data/lib/templates/new_app/config/routes.rb.erb +8 -3
- data/lib/templates/new_app/lambda/api.rb.erb +1 -1
- metadata +56 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 491ae37769b6a21cfd1823301d425d0af0b3f1e3c19952b909febebc7f2c3b32
|
|
4
|
+
data.tar.gz: a2427f68a34d38be0cf3af42ac33a8d3c3d190f36505c54ef531219cd0c3fb66
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 895fb4f7238141ba4752cdfc9200c44b2ef870bbc61585a436d3a9e36f39c689e8429d97ad4cf0101d92cbea06b3771864c1d31e4892dae54448a2f0e8ec138b
|
|
7
|
+
data.tar.gz: 4b2fee884c2f7ad9ba78bd7a46dacf75d1d5c8aa5da43d151ab7d03889aa740dd60a12a1fce5c18467f7c3bbb54aacb886a55efdcc58a997ed6367148ea37f15
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,246 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.15
|
|
4
|
+
|
|
5
|
+
### Bug Fix
|
|
6
|
+
|
|
7
|
+
- **`belt generate auth` NEW_PASSWORD_REQUIRED challenge sent `email` even when
|
|
8
|
+
the user already had it**: The 0.3.14 fix always passed `{ email }` to
|
|
9
|
+
`completeNewPasswordChallenge`. That fixed pools where `email` was missing, but
|
|
10
|
+
broke the common case where the user already has `email` set (e.g. created via
|
|
11
|
+
`admin-create-user` with an email) — Cognito rejects that with a 400
|
|
12
|
+
`NotAuthorizedException: Cannot modify an already provided email`. The template
|
|
13
|
+
now reads the `requiredAttributes` list Cognito provides in the
|
|
14
|
+
`newPasswordRequired` callback and supplies `email` **only when Cognito actually
|
|
15
|
+
requires it**. Users with email already set complete the challenge without
|
|
16
|
+
re-sending it; users missing email still get it supplied. Handles both cases.
|
|
17
|
+
|
|
18
|
+
## 0.3.14
|
|
19
|
+
|
|
20
|
+
### Bug Fix
|
|
21
|
+
|
|
22
|
+
- **`belt generate auth` NEW_PASSWORD_REQUIRED challenge dropped the email
|
|
23
|
+
attribute**: The generated `auth.js` `completeNewPassword` passed an empty
|
|
24
|
+
attributes object (`{}`) to `completeNewPasswordChallenge`. On user pools that
|
|
25
|
+
require `email`, admin-created users completing the `NEW_PASSWORD_REQUIRED`
|
|
26
|
+
challenge (which don't have `email` set yet) got a 400 `Invalid attributes
|
|
27
|
+
given, email is missing`. The template now supplies `{ email }` (the username
|
|
28
|
+
is the email) so the challenge completes. `Login.jsx` already passed the email
|
|
29
|
+
in, so no change was needed there.
|
|
30
|
+
|
|
31
|
+
## 0.3.13
|
|
32
|
+
|
|
33
|
+
### Security Fix
|
|
34
|
+
|
|
35
|
+
- **`belt generate auth` sent passwords in plaintext**: The generated `auth.js`
|
|
36
|
+
used the raw `@aws-sdk/client-cognito-identity-provider` with
|
|
37
|
+
`AuthFlow: 'USER_PASSWORD_AUTH'`, which puts the user's cleartext `PASSWORD` in
|
|
38
|
+
the `InitiateAuth` request payload (visible in devtools, proxies, and any request
|
|
39
|
+
logging). The template now uses `amazon-cognito-identity-js`, which authenticates
|
|
40
|
+
via SRP (Secure Remote Password) — the password never leaves the browser, only the
|
|
41
|
+
`SRP_A` proof is transmitted. `signIn` and the `NEW_PASSWORD_REQUIRED` challenge now
|
|
42
|
+
run over SRP; `signUp`/`confirmSignUp` are unchanged in behavior but route through the
|
|
43
|
+
same library.
|
|
44
|
+
- The generator now installs `amazon-cognito-identity-js` instead of
|
|
45
|
+
`@aws-sdk/client-cognito-identity-provider`.
|
|
46
|
+
- SRP requires the User Pool ID client-side, so `auth.js` now reads
|
|
47
|
+
`VITE_COGNITO_USER_POOL_ID` (already emitted by the auth generator's terraform
|
|
48
|
+
outputs and present in `env.yml.example`).
|
|
49
|
+
|
|
50
|
+
## 0.3.12
|
|
51
|
+
|
|
52
|
+
### Bug Fix
|
|
53
|
+
|
|
54
|
+
- **`env.yml.example` Cognito client ID typo**: The example env map referenced
|
|
55
|
+
`cognito_user_pool_client_id` as the terraform output name for
|
|
56
|
+
`VITE_COGNITO_CLIENT_ID`, but the correct output name is `cognito_client_id`.
|
|
57
|
+
Users following the tutorial would uncomment the line and get a missing-output
|
|
58
|
+
warning on deploy. Fixed in the template, CLI help text, and docs.
|
|
59
|
+
|
|
60
|
+
## 0.3.11
|
|
61
|
+
|
|
62
|
+
### Bug Fix
|
|
63
|
+
|
|
64
|
+
- **`belt deploy` skips route generation on first deploy**: The `generate_routes`
|
|
65
|
+
method would silently bail if `lambda/lib/routes/` didn't already exist on disk.
|
|
66
|
+
Since this directory is gitignored (it's a generated build artifact), a fresh clone
|
|
67
|
+
or first deploy would package the Lambda without route manifests, causing a
|
|
68
|
+
`LoadError` at cold start. Now creates the directory with `mkdir_p` before running
|
|
69
|
+
route generation.
|
|
70
|
+
|
|
71
|
+
## 0.3.10
|
|
72
|
+
|
|
73
|
+
### Bug Fix
|
|
74
|
+
|
|
75
|
+
- **`api_url` output missing base path for custom domains**: When a custom domain is
|
|
76
|
+
configured (e.g. `api.dev.example.com`), the `api_url` terraform output was missing
|
|
77
|
+
the gateway's base path segment. Conveyor-belt maps each gateway to a base path
|
|
78
|
+
(gateway "api" → path `/api`), so the correct URL is
|
|
79
|
+
`https://api.dev.example.com/api` not `https://api.dev.example.com`. The frontend
|
|
80
|
+
would get the wrong `VITE_API_URL` and hit a 403 on the custom domain root.
|
|
81
|
+
|
|
82
|
+
## 0.3.9
|
|
83
|
+
|
|
84
|
+
### Bug Fix
|
|
85
|
+
|
|
86
|
+
- **`belt setup frontend` with underscored app names**: The 0.3.8 fix placed
|
|
87
|
+
`local.s3_safe_name` in `dns.tf` but `belt setup frontend` only regenerates
|
|
88
|
+
`frontend.tf`, causing an "undeclared local value" error on existing apps.
|
|
89
|
+
The bucket name now uses inline `replace(var.app_name, "_", "-")` directly in
|
|
90
|
+
`frontend.tf`, making it fully self-contained with no cross-file dependency.
|
|
91
|
+
|
|
92
|
+
## 0.3.8
|
|
93
|
+
|
|
94
|
+
### Bug Fix
|
|
95
|
+
|
|
96
|
+
- **S3 bucket naming with underscored app names**: The module template now sanitizes
|
|
97
|
+
`var.app_name` for S3 bucket names by replacing underscores with hyphens. Apps named
|
|
98
|
+
with underscores (e.g. `feature_parity`) previously generated invalid bucket names
|
|
99
|
+
since S3 follows DNS naming rules which prohibit underscores.
|
|
100
|
+
|
|
101
|
+
## 0.3.7
|
|
102
|
+
|
|
103
|
+
### Bug Fix
|
|
104
|
+
|
|
105
|
+
- **App name detection in worktrees**: `detect_app_name` now checks
|
|
106
|
+
`infrastructure/*/variables.tf` for `variable "app_name" { default = "..." }`
|
|
107
|
+
before falling back to the directory name. Fixes incorrect Lambda function
|
|
108
|
+
lookups (e.g. `belt logs`) when running from a git worktree whose directory
|
|
109
|
+
name doesn't match the actual app name.
|
|
110
|
+
|
|
111
|
+
## 0.3.6
|
|
112
|
+
|
|
113
|
+
### Enhancement
|
|
114
|
+
|
|
115
|
+
- **Per-action request_model**: `request_model` on `resources` now accepts a Hash
|
|
116
|
+
to specify different validation schemas per action:
|
|
117
|
+
```ruby
|
|
118
|
+
resources :items, request_model: { create: :create_item, update: :update_item }
|
|
119
|
+
```
|
|
120
|
+
Symbol style (uniform for all body verbs) still works unchanged.
|
|
121
|
+
|
|
122
|
+
- **Convention-based request_model inference**: When no explicit `request_model` is
|
|
123
|
+
set on a resource route, `belt routes` auto-discovers matching contracts from
|
|
124
|
+
`contracts.rb` using a naming cascade:
|
|
125
|
+
1. `:<verb>_<gateway>_<singular_resource>` (e.g. `:create_customer_item`)
|
|
126
|
+
2. `:<verb>_<singular_resource>` (e.g. `:create_item`)
|
|
127
|
+
|
|
128
|
+
Only applies to body-accepting verbs (POST/PUT/PATCH). Explicit `request_model:`
|
|
129
|
+
always takes priority. No matching contract = no validation (same as before).
|
|
130
|
+
|
|
131
|
+
- **Convention-based response_model inference**: When no explicit `response_model` is
|
|
132
|
+
set on a resource route, `belt routes` auto-discovers a matching response model
|
|
133
|
+
from `contracts.rb` using the singular resource name.
|
|
134
|
+
e.g. `resources :items` → looks for `model :item` → auto-wires `response_model: :item`.
|
|
135
|
+
Applies to all verbs. Explicit `response_model:` always takes priority.
|
|
136
|
+
|
|
137
|
+
## 0.3.5
|
|
138
|
+
|
|
139
|
+
### Enhancement
|
|
140
|
+
|
|
141
|
+
- **Multiple frontends**: Frontend directory is no longer hardcoded to `frontend/`.
|
|
142
|
+
Declare named SPAs in `config/frontends.yml` (path, dist dir, terraform output names).
|
|
143
|
+
`belt generate frontend react --name ops --path ops-app` scaffolds an additional app.
|
|
144
|
+
Generators, `belt server`, `belt frontend env`, and `belt deploy frontend` accept
|
|
145
|
+
`--frontend NAME`. `belt deploy frontend <env>` deploys all configured frontends;
|
|
146
|
+
pass `--frontend` to target one. Existing single-`frontend/` apps keep working
|
|
147
|
+
with no config file.
|
|
148
|
+
- **Sidecar Lambda zips**: `belt deploy` / `belt plan` / `belt apply` build zip
|
|
149
|
+
artifacts that Terraform `filebase64sha256` needs at plan time (e.g. Stowzilla's
|
|
150
|
+
Node `image-processor`). Node packages are installed in Docker on linux/amd64
|
|
151
|
+
so native addons match Lambda. Missing zips are built automatically; unchanged
|
|
152
|
+
sources skip the rebuild.
|
|
153
|
+
|
|
154
|
+
### Bug fix
|
|
155
|
+
|
|
156
|
+
- **Frontend CloudFront invalidation**: `belt deploy frontend` no longer prints
|
|
157
|
+
terraform's "output not found" error when a named frontend sets
|
|
158
|
+
`cloudfront_domain_output` instead of a distribution-id output. Missing
|
|
159
|
+
terraform outputs are read with stderr silenced.
|
|
160
|
+
|
|
161
|
+
## 0.3.4
|
|
162
|
+
|
|
163
|
+
### Enhancement
|
|
164
|
+
|
|
165
|
+
- **Flexible config file extensions**: Lambda config files (`config/lambda/*.yml`) and frontend env maps (`frontend/env.yml`, `.belt/frontend_env.yml`) now also accept `.yaml` as a file extension. (#1124)
|
|
166
|
+
|
|
167
|
+
## 0.3.2
|
|
168
|
+
|
|
169
|
+
### Bug fix
|
|
170
|
+
|
|
171
|
+
- **Table name normalization**: Route JSON output now uses hyphens instead of underscores in inferred table names, matching the ActiveItem convention (e.g., `user-sessions` instead of `user_sessions`). (#51)
|
|
172
|
+
|
|
173
|
+
## 0.3.1
|
|
174
|
+
|
|
175
|
+
Version bump only — tagged release for internal tracking.
|
|
176
|
+
|
|
177
|
+
## 0.3.0
|
|
178
|
+
|
|
179
|
+
Version bump only — no code changes from 0.2.21.
|
|
180
|
+
|
|
181
|
+
**Why 0.3.0 instead of another patch?** The route DSL changes in 0.2.21 (`gateway`/`function`/`namespace`/`scope`) were a minor-version level change (new features, new keywords). RubyGems doesn't allow republishing the same version, so we're bumping to 0.3.0 to correct the semver trajectory.
|
|
182
|
+
|
|
183
|
+
If you're on 0.2.21, you already have the DSL changes. This release is purely for version hygiene.
|
|
184
|
+
|
|
185
|
+
## 0.2.21
|
|
186
|
+
|
|
187
|
+
### Route DSL: `gateway`, `function`, `namespace`, and `scope` keywords
|
|
188
|
+
|
|
189
|
+
The route DSL now uses terminology that matches AWS infrastructure:
|
|
190
|
+
|
|
191
|
+
- **`gateway`** — Creates an API Gateway with a default Lambda function of the same name (replaces top-level `namespace`)
|
|
192
|
+
- **`function`** — Targets enclosed routes to a different Lambda function
|
|
193
|
+
- **`namespace`** — Rails-like path + module prefix (e.g., `/admin/users` with `admin/users` controller). Does NOT change Lambda target.
|
|
194
|
+
- **`scope`** — Flexible grouping with `path:`, `module:`, `auth:`, `tables:` options. Does NOT change Lambda target.
|
|
195
|
+
|
|
196
|
+
```ruby
|
|
197
|
+
Belt.application.routes.draw do
|
|
198
|
+
gateway :api, auth: :cognito do
|
|
199
|
+
resources :posts # → lambda: "api", path: /posts
|
|
200
|
+
|
|
201
|
+
function :onboarding do
|
|
202
|
+
resources :steps # → lambda: "onboarding", path: /steps
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
namespace :admin do
|
|
206
|
+
resources :users # → lambda: "api", path: /admin/users, controller: "admin/users"
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
scope path: 'v2', module: 'v2' do
|
|
210
|
+
resources :widgets # → lambda: "api", path: /v2/widgets, controller: "v2/widgets"
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
**ActionRouter** now accepts `gateway:` keyword (preferred):
|
|
217
|
+
|
|
218
|
+
```ruby
|
|
219
|
+
ROUTER = Belt::ActionRouter.new(routes: Routes::API, gateway: 'api')
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
**100% backward compatible:** `namespace :api do` at top level still works (aliased to `gateway`), and `ActionRouter.new(namespace: 'api')` still works.
|
|
223
|
+
|
|
224
|
+
## 0.2.18
|
|
225
|
+
|
|
226
|
+
### New generator: `belt generate auth`
|
|
227
|
+
|
|
228
|
+
Scaffolds Cognito user pool infrastructure for authentication. Creates the user pool, client, and wires `cognito_user_pool_arns` into the conveyor-belt resource automatically.
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
belt g auth # Single pool: "main"
|
|
232
|
+
belt g auth web mobile # Multiple pools (e.g., web + mobile clients)
|
|
233
|
+
belt g auth web android ios # Three pools
|
|
234
|
+
belt destroy auth # Remove generated files
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
What it generates:
|
|
238
|
+
- `infrastructure/modules/app/cognito.tf` — User pool + client with sensible defaults (password policy, email verification, deletion protection in prod)
|
|
239
|
+
- `infrastructure/modules/app/cognito_outputs.tf` — Pool ID, ARN, and client ID outputs
|
|
240
|
+
- Patches `main.tf` to pass `cognito_user_pool_arns` to the conveyor-belt resource
|
|
241
|
+
|
|
242
|
+
Multiple pools are supported out of the box — each gets a suffixed name (e.g., `myapp-prod-web`, `myapp-prod-mobile`) and its own set of outputs.
|
|
243
|
+
|
|
3
244
|
## 0.2.12
|
|
4
245
|
|
|
5
246
|
### Rename: `routes.tf.rb` → `routes.rb`, `schema.tf.rb` → `contracts.rb`
|
data/README.md
CHANGED
|
@@ -105,7 +105,7 @@ require "belt"
|
|
|
105
105
|
|
|
106
106
|
include Belt::LambdaHandler
|
|
107
107
|
|
|
108
|
-
ROUTER = Belt::ActionRouter.new(routes: Routes::API,
|
|
108
|
+
ROUTER = Belt::ActionRouter.new(routes: Routes::API, gateway: "api")
|
|
109
109
|
|
|
110
110
|
def execute(path:, body:, event:)
|
|
111
111
|
ROUTER.route(event: event, body: body)
|
|
@@ -140,7 +140,7 @@ Define routes in `infrastructure/routes.tf.rb`:
|
|
|
140
140
|
|
|
141
141
|
```ruby
|
|
142
142
|
Belt.application.routes.draw do
|
|
143
|
-
|
|
143
|
+
gateway :api do
|
|
144
144
|
resources :posts, only: [:index, :show, :create]
|
|
145
145
|
end
|
|
146
146
|
end
|
|
@@ -170,6 +170,45 @@ The provider will:
|
|
|
170
170
|
- Generate IAM policies for DynamoDB table access
|
|
171
171
|
- Set up CloudWatch log groups
|
|
172
172
|
|
|
173
|
+
### Route DSL Keywords
|
|
174
|
+
|
|
175
|
+
The routes DSL has four keywords that map to infrastructure and code organization:
|
|
176
|
+
|
|
177
|
+
| Keyword | Purpose | Affects Lambda? |
|
|
178
|
+
|---------|---------|-----------------|
|
|
179
|
+
| `gateway` | Creates an API Gateway + default Lambda | Yes — sets the default Lambda for all routes inside |
|
|
180
|
+
| `function` | Routes to a different Lambda | Yes — overrides the gateway's default |
|
|
181
|
+
| `namespace` | Adds path prefix + controller module | No — Rails-like code organization only |
|
|
182
|
+
| `scope` | Flexible path/module/auth grouping | No — grouping and shared options only |
|
|
183
|
+
|
|
184
|
+
**Example combining all four:**
|
|
185
|
+
|
|
186
|
+
```ruby
|
|
187
|
+
Belt.application.routes.draw do
|
|
188
|
+
gateway :api, auth: :cognito do
|
|
189
|
+
resources :posts # → lambda: api, path: /posts, controller: posts
|
|
190
|
+
|
|
191
|
+
namespace :admin do
|
|
192
|
+
resources :users # → lambda: api, path: /admin/users, controller: admin/users
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
function :worker do
|
|
196
|
+
resources :jobs # → lambda: worker, path: /jobs, controller: jobs
|
|
197
|
+
|
|
198
|
+
namespace :internal do
|
|
199
|
+
resources :tasks # → lambda: worker, path: /internal/tasks, controller: internal/tasks
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
scope path: 'v2', module: 'legacy' do
|
|
204
|
+
resources :widgets # → lambda: api, path: /v2/widgets, controller: legacy/widgets
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
**Key point:** `namespace` and `scope` are purely organizational — they affect URL paths and controller module resolution but never change which Lambda handles the request. Use `function` when you need routes to go to a different Lambda.
|
|
211
|
+
|
|
173
212
|
## BeltController Features
|
|
174
213
|
|
|
175
214
|
### Callbacks
|
|
@@ -423,7 +462,7 @@ The command expects `infrastructure/routes.tf.rb` in the current working directo
|
|
|
423
462
|
|
|
424
463
|
```ruby
|
|
425
464
|
Belt.application.routes.draw do
|
|
426
|
-
|
|
465
|
+
gateway :api do
|
|
427
466
|
resources :posts, only: [:index, :show, :create, :destroy]
|
|
428
467
|
resource :profile, only: [:show, :update]
|
|
429
468
|
get "health", action: :health
|
data/lib/belt/action_router.rb
CHANGED
|
@@ -7,28 +7,39 @@ module Belt
|
|
|
7
7
|
# Routes incoming requests to controllers based on a route manifest.
|
|
8
8
|
#
|
|
9
9
|
# Usage:
|
|
10
|
-
# ROUTER = Belt::ActionRouter.new(routes: MY_ROUTES,
|
|
10
|
+
# ROUTER = Belt::ActionRouter.new(routes: MY_ROUTES, gateway: "api")
|
|
11
11
|
# response = ROUTER.route(event: event, body: body)
|
|
12
12
|
#
|
|
13
13
|
class ActionRouter
|
|
14
14
|
class RouteNotFound < StandardError; end
|
|
15
15
|
|
|
16
|
-
def initialize(routes:, namespace:)
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
def initialize(routes:, gateway: nil, namespace: nil)
|
|
17
|
+
# Accept `gateway:` (preferred) or legacy `namespace:` keyword
|
|
18
|
+
gw = gateway || namespace
|
|
19
|
+
raise ArgumentError, 'Belt::ActionRouter requires a gateway: (or legacy namespace:) argument' unless gw
|
|
20
|
+
|
|
21
|
+
@gateway = gw.to_s
|
|
22
|
+
@gateway_module_name = "#{@gateway.split('_').map(&:capitalize).join}Controllers"
|
|
19
23
|
@routes = build_route_table(routes)
|
|
20
24
|
end
|
|
21
25
|
|
|
22
26
|
def route(event:, body:)
|
|
23
27
|
method = event['httpMethod']
|
|
24
28
|
full_path = event['path']
|
|
25
|
-
match_path =
|
|
29
|
+
match_path = strip_gateway_prefix(full_path)
|
|
26
30
|
|
|
27
31
|
route_info = find_route(method, match_path)
|
|
28
32
|
|
|
29
33
|
unless route_info
|
|
30
|
-
Belt
|
|
31
|
-
|
|
34
|
+
# Fallback: serve the Belt welcome page for GET / when no explicit root route exists.
|
|
35
|
+
# This handles the case where API Gateway delivers the request (e.g. via a catch-all
|
|
36
|
+
# integration or proxy route) but the route manifest doesn't include GET /.
|
|
37
|
+
if method == 'GET' && root_path?(match_path)
|
|
38
|
+
route_info = { verb: 'GET', pattern: '/', segments: [], controller: 'welcome', action: 'show' }
|
|
39
|
+
else
|
|
40
|
+
Belt::Observability::Logger.instance&.warn('Route not found', method: method, path: full_path)
|
|
41
|
+
return error_response('Not found', 404, event)
|
|
42
|
+
end
|
|
32
43
|
end
|
|
33
44
|
|
|
34
45
|
path_params = extract_path_params(route_info[:pattern], match_path)
|
|
@@ -56,10 +67,14 @@ module Belt
|
|
|
56
67
|
|
|
57
68
|
private
|
|
58
69
|
|
|
59
|
-
def
|
|
70
|
+
def root_path?(path)
|
|
71
|
+
path == '/' || path.empty?
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def strip_gateway_prefix(path)
|
|
60
75
|
return '/' if path.nil?
|
|
61
76
|
|
|
62
|
-
prefix = "/#{@
|
|
77
|
+
prefix = "/#{@gateway}"
|
|
63
78
|
if path.start_with?(prefix)
|
|
64
79
|
stripped = path.sub(prefix, '')
|
|
65
80
|
stripped.empty? ? '/' : stripped
|
|
@@ -127,15 +142,15 @@ module Belt
|
|
|
127
142
|
return false if name.start_with?('/') || name.end_with?('/')
|
|
128
143
|
return false if name.include?('\\')
|
|
129
144
|
|
|
130
|
-
# Allow only lowercase letters, digits, underscores, and
|
|
145
|
+
# Allow only lowercase letters, digits, underscores, and forward slashes for nesting
|
|
131
146
|
name.match?(%r{\A[a-z][a-z0-9_]*(/[a-z][a-z0-9_]*)?\z})
|
|
132
147
|
end
|
|
133
148
|
|
|
134
149
|
def resolve_controller(controller_name)
|
|
135
|
-
# Try
|
|
150
|
+
# Try gateway module first (app's own controllers)
|
|
136
151
|
begin
|
|
137
|
-
|
|
138
|
-
return resolve_from_module(
|
|
152
|
+
gateway_module = Object.const_get(@gateway_module_name)
|
|
153
|
+
return resolve_from_module(gateway_module, controller_name)
|
|
139
154
|
rescue NameError
|
|
140
155
|
# Fall through to controller_paths lookup
|
|
141
156
|
end
|
|
@@ -144,13 +159,13 @@ module Belt
|
|
|
144
159
|
resolve_from_paths(controller_name)
|
|
145
160
|
end
|
|
146
161
|
|
|
147
|
-
def resolve_from_module(
|
|
162
|
+
def resolve_from_module(gateway_module, controller_name)
|
|
148
163
|
if controller_name.include?('/')
|
|
149
164
|
parts = controller_name.split('/')
|
|
150
|
-
parent =
|
|
165
|
+
parent = gateway_module.const_get(parts[0].split('_').map(&:capitalize).join)
|
|
151
166
|
parent.const_get("#{parts[1].split('_').map(&:capitalize).join}Controller")
|
|
152
167
|
else
|
|
153
|
-
|
|
168
|
+
gateway_module.const_get("#{controller_name.split('_').map(&:capitalize).join}Controller")
|
|
154
169
|
end
|
|
155
170
|
end
|
|
156
171
|
|
|
@@ -176,9 +191,9 @@ module Belt
|
|
|
176
191
|
class_name = "#{controller_name.split(%r{[_/]}).map(&:capitalize).join}Controller"
|
|
177
192
|
return Object.const_get(class_name) if Object.const_defined?(class_name)
|
|
178
193
|
|
|
179
|
-
# Try under
|
|
180
|
-
if Object.const_defined?(@
|
|
181
|
-
ns = Object.const_get(@
|
|
194
|
+
# Try under gateway module (e.g., ApiControllers::PostsController)
|
|
195
|
+
if Object.const_defined?(@gateway_module_name)
|
|
196
|
+
ns = Object.const_get(@gateway_module_name)
|
|
182
197
|
return ns.const_get(class_name) if ns.const_defined?(class_name)
|
|
183
198
|
end
|
|
184
199
|
end
|
|
@@ -3,19 +3,21 @@
|
|
|
3
3
|
module Belt
|
|
4
4
|
module CLI
|
|
5
5
|
module AppDetection
|
|
6
|
-
# Detects the primary
|
|
6
|
+
# Detects the primary gateway from the route definitions.
|
|
7
7
|
# Used by generators to determine controller directory and route file naming.
|
|
8
8
|
def detect_namespace
|
|
9
9
|
routes_file = find_routes_file_path
|
|
10
10
|
if routes_file && File.exist?(routes_file)
|
|
11
|
-
|
|
11
|
+
content = File.read(routes_file)
|
|
12
|
+
# Prefer `gateway :name` but fall back to legacy `namespace :name` at top-level
|
|
13
|
+
match = content.match(/^\s*gateway :(\w+)/) || content.match(/^\s*namespace :(\w+)/)
|
|
12
14
|
return match[1] if match
|
|
13
15
|
end
|
|
14
16
|
File.basename(Dir.pwd)
|
|
15
17
|
end
|
|
16
18
|
|
|
17
19
|
# Detects the application name.
|
|
18
|
-
#
|
|
20
|
+
# Priority: terraform.tfvars app_name > variables.tf default > directory name.
|
|
19
21
|
def detect_app_name
|
|
20
22
|
# Check any environment tfvars for the app_name
|
|
21
23
|
Dir.glob('infrastructure/*/terraform.tfvars').each do |f|
|
|
@@ -24,6 +26,10 @@ module Belt
|
|
|
24
26
|
return match[1] if match
|
|
25
27
|
end
|
|
26
28
|
|
|
29
|
+
# Check variables.tf for a default app_name value
|
|
30
|
+
name = detect_app_name_from_variables_tf
|
|
31
|
+
return name if name
|
|
32
|
+
|
|
27
33
|
# Fall back to directory name
|
|
28
34
|
File.basename(Dir.pwd)
|
|
29
35
|
end
|
|
@@ -65,6 +71,27 @@ module Belt
|
|
|
65
71
|
def find_schema_file_path
|
|
66
72
|
find_contracts_file_path
|
|
67
73
|
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
# Parses `variable "app_name" { default = "..." }` from any environment's variables.tf.
|
|
78
|
+
# This handles the common pattern where app_name is a Terraform variable with a default
|
|
79
|
+
# rather than being set directly in terraform.tfvars.
|
|
80
|
+
def detect_app_name_from_variables_tf
|
|
81
|
+
Dir.glob('infrastructure/*/variables.tf').each do |f|
|
|
82
|
+
content = File.read(f)
|
|
83
|
+
# Match the app_name variable block and extract its default value
|
|
84
|
+
next unless content.match?(/variable\s+"app_name"/)
|
|
85
|
+
|
|
86
|
+
# Extract default from within the variable block
|
|
87
|
+
block_match = content.match(/variable\s+"app_name"\s*\{([^}]*)\}/m)
|
|
88
|
+
next unless block_match
|
|
89
|
+
|
|
90
|
+
default_match = block_match[1].match(/default\s*=\s*"([^"]+)"/)
|
|
91
|
+
return default_match[1] if default_match
|
|
92
|
+
end
|
|
93
|
+
nil
|
|
94
|
+
end
|
|
68
95
|
end
|
|
69
96
|
end
|
|
70
97
|
end
|