belt 0.3.2 → 0.3.3

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,128 @@
1
+ # Generators
2
+
3
+ Belt generators scaffold code and infrastructure for common patterns.
4
+ Run `belt generate --help` to see all available generators (built-in + plugins).
5
+
6
+ ## Built-in Generators
7
+
8
+ ### Resource (scaffold)
9
+
10
+ Creates model + controller + routes + schema entry:
11
+
12
+ ```bash
13
+ belt generate resource post title:string body:text user_id:string status:string
14
+ belt g resource comment body:text author:string post_id:string
15
+ ```
16
+
17
+ This is the most common generator — it sets up everything for a new REST endpoint.
18
+
19
+ ### Model
20
+
21
+ Creates just the model file + schema entry:
22
+
23
+ ```bash
24
+ belt generate model post title:string body:text user_id:string
25
+ belt g model payment amount:number currency:string
26
+ ```
27
+
28
+ ### Controller
29
+
30
+ Creates just the controller:
31
+
32
+ ```bash
33
+ belt generate controller posts
34
+ belt g controller admin/users
35
+ ```
36
+
37
+ ### Environment
38
+
39
+ Scaffolds a new Terraform environment directory:
40
+
41
+ ```bash
42
+ belt generate environment staging
43
+ belt g environment prod
44
+ ```
45
+
46
+ ### Frontend
47
+
48
+ Adds a frontend framework to the project:
49
+
50
+ ```bash
51
+ belt generate frontend react
52
+ belt generate frontend vue
53
+ belt generate frontend svelte
54
+ ```
55
+
56
+ ### Views
57
+
58
+ Generates React pages for a resource's REST actions:
59
+
60
+ ```bash
61
+ belt generate views post title:string body:text status:string
62
+ belt g views comment body:text author:string
63
+ ```
64
+
65
+ ### Auth
66
+
67
+ Sets up Cognito authentication:
68
+
69
+ ```bash
70
+ belt generate auth
71
+ belt g auth --provider cognito
72
+ ```
73
+
74
+ ## Field Types
75
+
76
+ When specifying fields, use these types:
77
+
78
+ | Type | DynamoDB Type | Notes |
79
+ |------|---------------|-------|
80
+ | `string` | S | Default if no type given |
81
+ | `text` | S | Same as string (semantic hint) |
82
+ | `number` | N | Numeric values |
83
+ | `boolean` | BOOL | True/false |
84
+ | `list` | L | Array values |
85
+ | `map` | M | Nested objects |
86
+
87
+ ## Destroying Generated Code
88
+
89
+ Every generator has a matching destroy command:
90
+
91
+ ```bash
92
+ belt destroy resource post
93
+ belt destroy model comment
94
+ belt destroy controller admin/users
95
+ belt destroy environment staging
96
+ belt destroy frontend
97
+ belt destroy views post
98
+ ```
99
+
100
+ ## Plugin Generators
101
+
102
+ Gems that follow the Belt plugin contract are auto-discovered:
103
+
104
+ ```bash
105
+ belt generate messaging # from belt-messaging gem
106
+ belt generate pay # from belt-pay gem
107
+ belt generate --help # lists all available generators
108
+ ```
109
+
110
+ ## Generator Workflow (Common Pattern)
111
+
112
+ ```bash
113
+ # 1. Generate the resource
114
+ belt generate resource order item_id:string quantity:number total:number status:string
115
+
116
+ # 2. Generate DynamoDB table from schema
117
+ belt setup tables dev01
118
+
119
+ # 3. Deploy
120
+ belt deploy dev01
121
+ ```
122
+
123
+ ## See Also
124
+
125
+ - `belt explain models` — how generated models work
126
+ - `belt explain controllers` — how generated controllers work
127
+ - `belt explain routing` — how generated routes work
128
+ - `belt explain deployment` — deploying generated code
@@ -0,0 +1,105 @@
1
+ # Lambda Handler
2
+
3
+ `Belt::LambdaHandler` is the module you include in your Lambda entry point.
4
+ It provides the `lambda_handler` method that AWS Lambda invokes, wrapping
5
+ your application logic with observability, CORS, and error handling.
6
+
7
+ ## Basic Usage
8
+
9
+ ```ruby
10
+ require "belt"
11
+
12
+ include Belt::LambdaHandler
13
+
14
+ ROUTER = Belt::ActionRouter.new(routes: Routes::API, gateway: "api")
15
+
16
+ def execute(path:, body:, event:)
17
+ ROUTER.route(event: event, body: body)
18
+ end
19
+ ```
20
+
21
+ ## What It Does
22
+
23
+ When a request arrives, `lambda_handler` automatically:
24
+
25
+ 1. **Initializes observability** — structured logging + CloudWatch metrics
26
+ 2. **Handles OPTIONS preflight** — returns CORS headers immediately
27
+ 3. **Parses the request body** — JSON string → Ruby hash
28
+ 4. **Calls your `execute` method** — with `path:`, `body:`, and `event:`
29
+ 5. **Catches unhandled errors** — returns a CORS-enabled error response
30
+ 6. **Emits metrics** — request count, latency, error count via EMF
31
+
32
+ ## The `execute` Method
33
+
34
+ You must define `execute` in your Lambda file. It receives:
35
+
36
+ | Param | Description |
37
+ |-------|-------------|
38
+ | `path:` | The request path (e.g., `/posts/123`) |
39
+ | `body:` | Parsed request body (Hash or nil) |
40
+ | `event:` | Full API Gateway event (for headers, query params, auth context) |
41
+
42
+ Return value should be a response hash: `{ statusCode:, headers:, body: }`.
43
+ Typically you just call `ROUTER.route(...)` which returns the correct format.
44
+
45
+ ## Multiple Lambdas
46
+
47
+ If your app has multiple Lambda functions (via `function` in routes):
48
+
49
+ ```ruby
50
+ # lambda/worker.rb
51
+ require "belt"
52
+
53
+ include Belt::LambdaHandler
54
+
55
+ ROUTER = Belt::ActionRouter.new(routes: Routes::WORKER, gateway: "worker")
56
+
57
+ def execute(path:, body:, event:)
58
+ ROUTER.route(event: event, body: body)
59
+ end
60
+ ```
61
+
62
+ Each Lambda entry point gets its own route manifest and gateway name.
63
+
64
+ ## Configuration
65
+
66
+ Configure via `config/lambda/<name>.yml`:
67
+
68
+ ```yaml
69
+ handler: lambda/api.lambda_handler
70
+ runtime: ruby3.3
71
+ timeout: 30
72
+ memory: 256
73
+ layers:
74
+ - ${var.ruby_layer_arn}
75
+ environment:
76
+ ENVIRONMENT: ${var.environment}
77
+ APP_NAME: my-app
78
+ ERROR_NOTIFICATION_TOPIC_ARN: ${var.sns_topic_arn}
79
+ ```
80
+
81
+ ## Observability
82
+
83
+ The handler sets up `Belt::Observability::Logger` and `Belt::Observability::Metrics`
84
+ automatically. Use them anywhere in your app:
85
+
86
+ ```ruby
87
+ Belt::Observability::Logger.info("Order created", order_id: order.id)
88
+ Belt::Observability::Metrics.track_event("OrderCreated", model: "Order")
89
+ ```
90
+
91
+ ## Environment Variables
92
+
93
+ | Variable | Purpose |
94
+ |----------|---------|
95
+ | `ENVIRONMENT` | Controls error verbosity (`dev*`, `local`, `test` = verbose) |
96
+ | `BELT_METRICS_NAMESPACE` | CloudWatch namespace (default: `Belt`) |
97
+ | `ACTION` | Service name for logging |
98
+ | `ERROR_NOTIFICATION_TOPIC_ARN` | SNS topic for error alerts |
99
+ | `CORS_ALLOWED_ORIGINS` | Comma-separated allowed origins |
100
+
101
+ ## See Also
102
+
103
+ - `belt explain routing` — how ActionRouter dispatches requests
104
+ - `belt explain controllers` — how dispatched requests are handled
105
+ - `belt explain deployment` — Lambda packaging and configuration
@@ -0,0 +1,160 @@
1
+ # Models
2
+
3
+ Belt uses **ActiveItem** as its ORM for DynamoDB. Models inherit from
4
+ `ActiveItem::Base` (typically via an `ApplicationRecord` base class).
5
+
6
+ ## Basic Model
7
+
8
+ ```ruby
9
+ class Post < ApplicationRecord
10
+ attr_accessor :id, :user_id, :title, :body, :status, :created_at, :updated_at
11
+
12
+ validates :title, presence: true
13
+ validates :status, inclusion: { in: %w[draft published] }, allow_nil: true
14
+
15
+ before_create { self.id ||= SecureRandom.uuid }
16
+ before_save { self.updated_at = Time.now.iso8601 }
17
+ end
18
+ ```
19
+
20
+ ## Table Naming
21
+
22
+ Table names are derived from the environment:
23
+ `{APP_NAME}-{ENVIRONMENT}-{pluralized_model}`
24
+
25
+ Example: app "blog", environment "prod", model "Post" → `blog-prod-posts`
26
+
27
+ ## Schema Definition
28
+
29
+ Define table structure in `infrastructure/schema.tf.rb`:
30
+
31
+ ```ruby
32
+ Belt.application.schema.define do
33
+ model :post do
34
+ partition_key :id, :string
35
+ global_secondary_index :UserIndex, partition_key: :user_id
36
+ global_secondary_index :StatusIndex, partition_key: :status, sort_key: :created_at
37
+ end
38
+ end
39
+ ```
40
+
41
+ ## CRUD Operations
42
+
43
+ ### Create
44
+
45
+ ```ruby
46
+ post = Post.create!(title: "Hello", body: "World", user_id: "u-123")
47
+ # or
48
+ post = Post.new(title: "Hello")
49
+ post.save!
50
+ ```
51
+
52
+ ### Read
53
+
54
+ ```ruby
55
+ post = Post.find("post-id-123") # by primary key
56
+ posts = Post.all # scan (use sparingly)
57
+ posts = Post.where(user_id: "u-123", index: "UserIndex")
58
+ post = Post.find_by(user_id: "u-123", index: "UserIndex") # first match
59
+ ```
60
+
61
+ ### Update
62
+
63
+ ```ruby
64
+ post = Post.find("post-id-123")
65
+ post.update(title: "New Title")
66
+ # or
67
+ post.title = "New Title"
68
+ post.save!
69
+ ```
70
+
71
+ ### Delete
72
+
73
+ ```ruby
74
+ post = Post.find("post-id-123")
75
+ post.destroy
76
+ ```
77
+
78
+ ## Query Patterns
79
+
80
+ ### Using Indexes
81
+
82
+ ```ruby
83
+ # Query a GSI
84
+ Post.where(user_id: "u-123", index: "UserIndex")
85
+
86
+ # With sort key conditions
87
+ Post.where(
88
+ status: "published",
89
+ created_at: { gte: "2024-01-01" },
90
+ index: "StatusIndex"
91
+ )
92
+ ```
93
+
94
+ ### Count and Existence
95
+
96
+ ```ruby
97
+ Post.count # total items (scan)
98
+ Post.exists?("post-id-123") # check by primary key
99
+ ```
100
+
101
+ ## Validations
102
+
103
+ ActiveItem supports ActiveModel-style validations:
104
+
105
+ ```ruby
106
+ validates :title, presence: true
107
+ validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }
108
+ validates :status, inclusion: { in: %w[active inactive] }
109
+ validates :age, numericality: { greater_than: 0 }
110
+ ```
111
+
112
+ ## Callbacks
113
+
114
+ ```ruby
115
+ before_create { self.id ||= SecureRandom.uuid }
116
+ before_save { self.updated_at = Time.now.iso8601 }
117
+ after_create { notify_subscribers }
118
+ before_destroy { cleanup_associations }
119
+ ```
120
+
121
+ ## Associations (Manual)
122
+
123
+ DynamoDB doesn't have joins. Model relationships manually:
124
+
125
+ ```ruby
126
+ class Post < ApplicationRecord
127
+ def comments
128
+ Comment.where(post_id: id, index: "PostIndex")
129
+ end
130
+
131
+ def author
132
+ User.find(user_id)
133
+ end
134
+ end
135
+ ```
136
+
137
+ ## Transactions
138
+
139
+ ```ruby
140
+ ActiveItem::Transaction.write do |tx|
141
+ tx.put(post)
142
+ tx.put(comment)
143
+ tx.delete(draft)
144
+ end
145
+ ```
146
+
147
+ ## Generating Models
148
+
149
+ ```bash
150
+ belt generate model post title:string body:text user_id:string
151
+ belt generate resource comment body:text author:string post_id:string
152
+ ```
153
+
154
+ The resource generator creates model + controller + routes + schema entry.
155
+
156
+ ## See Also
157
+
158
+ - `belt explain controllers` — using models in controllers
159
+ - `belt explain deployment` — how schema becomes infrastructure
160
+ - `belt explain queries` — advanced DynamoDB query patterns
@@ -0,0 +1,94 @@
1
+ # Observability
2
+
3
+ Belt provides structured logging and CloudWatch metrics out of the box.
4
+ These are initialized automatically by `Belt::LambdaHandler` — no setup required.
5
+
6
+ ## Logging
7
+
8
+ Access the logger from anywhere:
9
+
10
+ ```ruby
11
+ Belt::Observability::Logger.info("Order placed", order_id: "o-123", total: 49.99)
12
+ Belt::Observability::Logger.warn("Retry attempt", attempt: 3, service: "payments")
13
+ Belt::Observability::Logger.error("Payment failed", error: e.message, order_id: "o-123")
14
+ ```
15
+
16
+ Logs are structured JSON, compatible with CloudWatch Logs Insights:
17
+
18
+ ```json
19
+ {
20
+ "level": "INFO",
21
+ "message": "Order placed",
22
+ "order_id": "o-123",
23
+ "total": 49.99,
24
+ "timestamp": "2024-01-15T10:30:00Z",
25
+ "service": "api"
26
+ }
27
+ ```
28
+
29
+ ## Metrics
30
+
31
+ Track custom events via CloudWatch Embedded Metric Format (EMF):
32
+
33
+ ```ruby
34
+ Belt::Observability::Metrics.track_event("OrderCreated", model: "Order")
35
+ Belt::Observability::Metrics.track_event("PaymentProcessed", amount: 49.99)
36
+ ```
37
+
38
+ ### Built-in Metrics
39
+
40
+ The Lambda handler automatically emits:
41
+ - `RequestCount` — per invocation
42
+ - `ErrorCount` — on unhandled exceptions
43
+ - `Latency` — request duration in milliseconds
44
+
45
+ ### Namespace
46
+
47
+ Metrics are published under the namespace set by `BELT_METRICS_NAMESPACE`
48
+ (defaults to `Belt`). View them in CloudWatch → Metrics → Custom namespaces.
49
+
50
+ ## Error Alerting
51
+
52
+ Set `ERROR_NOTIFICATION_TOPIC_ARN` to an SNS topic ARN. Unhandled errors
53
+ will publish a notification with error details (message, backtrace, request context).
54
+
55
+ ```yaml
56
+ # config/lambda/api.yml
57
+ environment:
58
+ ERROR_NOTIFICATION_TOPIC_ARN: ${var.sns_topic_arn}
59
+ ```
60
+
61
+ ## CloudWatch Logs Insights
62
+
63
+ Query structured logs:
64
+
65
+ ```
66
+ fields @timestamp, message, order_id
67
+ | filter level = "ERROR"
68
+ | sort @timestamp desc
69
+ | limit 20
70
+ ```
71
+
72
+ ## Viewing Logs Locally
73
+
74
+ ```bash
75
+ belt logs api # tail logs for the "api" Lambda
76
+ belt logs api -f # follow (live tail)
77
+ belt logs api -s 30m # last 30 minutes
78
+ belt logs worker -e prod # specific environment
79
+ ```
80
+
81
+ ## Environment Variables
82
+
83
+ | Variable | Purpose |
84
+ |----------|---------|
85
+ | `BELT_METRICS_NAMESPACE` | CloudWatch metrics namespace (default: `Belt`) |
86
+ | `ACTION` | Service name in log entries (falls back to function name) |
87
+ | `ERROR_NOTIFICATION_TOPIC_ARN` | SNS topic for error alerts |
88
+ | `ENVIRONMENT` | Controls verbose errors (`dev*`/`local`/`test` = verbose) |
89
+
90
+ ## See Also
91
+
92
+ - `belt explain lambda_handler` — how observability is initialized
93
+ - `belt explain deployment` — configuring environment variables
94
+ - `belt logs --help` — full CLI options for log viewing
@@ -0,0 +1,94 @@
1
+ # Plugins
2
+
3
+ Belt plugins are separate gems that extend the CLI and runtime. They're
4
+ discovered automatically — no registration file or initializer hook needed.
5
+
6
+ ## Using a Plugin
7
+
8
+ ```ruby
9
+ # Gemfile
10
+ gem "belt-messaging"
11
+ ```
12
+
13
+ ```bash
14
+ bundle install
15
+ belt generate messaging # run the plugin's generator
16
+ belt destroy messaging # remove what the generator created
17
+ ```
18
+
19
+ ## Available Plugins
20
+
21
+ | Gem | Purpose |
22
+ |-----|---------|
23
+ | `belt-messaging` | Two-way SMS via AWS End User Messaging |
24
+ | `belt-pay` | Stripe payments & subscriptions |
25
+
26
+ ## Creating a Plugin
27
+
28
+ Scaffold a new plugin gem:
29
+
30
+ ```bash
31
+ belt plugin new notifications
32
+ belt plugin new pay --path ~/Code --summary "Stripe payments for Belt"
33
+ ```
34
+
35
+ This creates a gem with the correct structure and Belt integration points.
36
+
37
+ ## Plugin Discovery Contract
38
+
39
+ Belt discovers plugins automatically when:
40
+
41
+ 1. Gem is in the app's `Gemfile` and bundled
42
+ 2. Gem has a file at `lib/belt/generators/<name>_generator.rb`
43
+ 3. Class is `Belt::Generators::<Name>Generator`
44
+ 4. Class implements `.run(args)` (required)
45
+
46
+ Optional: `.destroy(args)` and `.description` for destroy path and help text.
47
+
48
+ ## Generator Checklist
49
+
50
+ A good plugin generator typically installs:
51
+
52
+ 1. **Terraform module** → `infrastructure/modules/<name>/`
53
+ 2. **Lambda config** → `config/lambda/<name>.yml`
54
+ 3. **Lambda entrypoint** → `lambda/<name>.rb` using `Belt::LambdaHandler`
55
+ 4. **Routes / schema** → inject into routes or schema when needed
56
+ 5. **Optional overrides** → `--controllers` flag for app-local subclasses
57
+ 6. **Destroy path** → removes everything the generator created
58
+ 7. **Help text** → `.description` + `--help`
59
+
60
+ ## Plugin Layout
61
+
62
+ ```
63
+ belt-messaging/
64
+ ├── belt-messaging.gemspec
65
+ ├── lib/
66
+ │ ├── belt-messaging.rb # require entrypoint
67
+ │ └── belt/
68
+ │ ├── messaging.rb # Runtime API
69
+ │ ├── messaging/
70
+ │ │ ├── configuration.rb
71
+ │ │ ├── version.rb
72
+ │ │ ├── controllers/ # Default controllers
73
+ │ │ └── templates/ # ERB templates for generator
74
+ │ └── generators/
75
+ │ └── messaging_generator.rb # ← auto-discovered
76
+ └── spec/
77
+ ```
78
+
79
+ ## Development Workflow
80
+
81
+ Point a Belt app at your plugin during development:
82
+
83
+ ```ruby
84
+ # In the app Gemfile
85
+ gem "belt-notifications", path: "../belt-notifications"
86
+ ```
87
+
88
+ `belt deploy` detects `path:` gems and vendors them into `vendor/cache`
89
+ for Lambda packaging.
90
+
91
+ ## See Also
92
+
93
+ - `belt explain generators` — built-in generators
94
+ - `belt plugin new --help` — scaffold options
@@ -0,0 +1,138 @@
1
+ # Routing
2
+
3
+ Belt routes map HTTP requests to controller actions. Routes are defined in
4
+ `config/routes.rb` using a Ruby DSL that mirrors infrastructure (API Gateway + Lambda).
5
+
6
+ ## Defining Routes
7
+
8
+ ```ruby
9
+ Belt.application.routes.draw do
10
+ gateway :api do
11
+ resources :posts
12
+ resources :comments, only: [:index, :create]
13
+ resource :profile, only: [:show, :update]
14
+ get "health", action: :health
15
+ end
16
+ end
17
+ ```
18
+
19
+ ## Route DSL Keywords
20
+
21
+ | Keyword | Purpose | Creates Lambda? |
22
+ |---------|---------|-----------------|
23
+ | `gateway` | Creates an API Gateway + default Lambda | Yes |
24
+ | `function` | Routes to a different Lambda (overrides gateway default) | Yes |
25
+ | `namespace` | Adds path prefix + controller module nesting | No |
26
+ | `scope` | Flexible path/module/auth grouping | No |
27
+
28
+ ## How `resources` Maps to Verbs
29
+
30
+ `resources :posts` generates:
31
+
32
+ | Verb | Path | Action |
33
+ |------|------|--------|
34
+ | GET | /posts | index |
35
+ | POST | /posts | create |
36
+ | GET | /posts/{post_id} | show |
37
+ | PUT | /posts/{post_id} | update |
38
+ | DELETE | /posts/{post_id} | destroy |
39
+
40
+ **Note:** Belt uses PUT, not PATCH, for updates.
41
+
42
+ Use `only:` or `except:` to limit generated routes:
43
+
44
+ ```ruby
45
+ resources :posts, only: [:index, :show, :create]
46
+ resources :comments, except: [:destroy]
47
+ ```
48
+
49
+ ## Singular Resources
50
+
51
+ `resource :profile` (no `:id` in the path):
52
+
53
+ | Verb | Path | Action |
54
+ |------|------|--------|
55
+ | GET | /profile | show |
56
+ | PUT | /profile | update |
57
+ | POST | /profile | create |
58
+ | DELETE | /profile | destroy |
59
+
60
+ ## Namespace and Scope
61
+
62
+ ```ruby
63
+ gateway :api do
64
+ # Namespace: adds path prefix AND controller module
65
+ namespace :admin do
66
+ resources :users # → /admin/users → Admin::UsersController
67
+ end
68
+
69
+ # Scope: flexible grouping without full nesting
70
+ scope path: 'v2', module: 'legacy' do
71
+ resources :widgets # → /v2/widgets → Legacy::WidgetsController
72
+ end
73
+ end
74
+ ```
75
+
76
+ ## Multiple Lambdas
77
+
78
+ Use `function` when routes should be handled by a separate Lambda:
79
+
80
+ ```ruby
81
+ gateway :api do
82
+ resources :posts # → handled by "api" Lambda
83
+
84
+ function :worker do
85
+ resources :jobs # → handled by "worker" Lambda
86
+ end
87
+ end
88
+ ```
89
+
90
+ ## Authentication
91
+
92
+ ```ruby
93
+ gateway :api, auth: :cognito do
94
+ resources :posts # requires cognito auth
95
+ get "health", action: :health, auth: :none # public
96
+ end
97
+ ```
98
+
99
+ ## Table Access
100
+
101
+ Declare which DynamoDB tables a route accesses (used by Terraform for IAM):
102
+
103
+ ```ruby
104
+ resources :posts, tables: [:posts, :comments]
105
+ ```
106
+
107
+ ## Inspecting Routes
108
+
109
+ ```bash
110
+ belt routes # display all routes
111
+ belt routes -g posts # filter by pattern
112
+ belt routes -f json # machine-readable output
113
+ belt routes --namespace api # generate Ruby route manifest
114
+ ```
115
+
116
+ ## Runtime Routing
117
+
118
+ The Lambda entry point uses `Belt::ActionRouter` with the generated route manifest:
119
+
120
+ ```ruby
121
+ require "belt"
122
+ include Belt::LambdaHandler
123
+
124
+ ROUTER = Belt::ActionRouter.new(routes: Routes::API, gateway: "api")
125
+
126
+ def execute(path:, body:, event:)
127
+ ROUTER.route(event: event, body: body)
128
+ end
129
+ ```
130
+
131
+ The router matches the incoming HTTP method + path against the manifest and
132
+ dispatches to the appropriate controller and action.
133
+
134
+ ## See Also
135
+
136
+ - `belt explain controllers` — how controllers handle requests
137
+ - `belt explain deployment` — how routes become infrastructure
138
+ - `belt routes --help` — full CLI options