belt 0.2.11 → 0.2.13
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 +42 -1
- data/lib/belt/cli/app_detection.rb +19 -5
- data/lib/belt/cli/contracts_command.rb +142 -0
- data/lib/belt/cli/destroy_command.rb +74 -0
- data/lib/belt/cli/frontend_command.rb +23 -0
- data/lib/belt/cli/generate_command.rb +11 -9
- data/lib/belt/cli/logs_command.rb +634 -0
- data/lib/belt/cli/new_command.rb +2 -2
- data/lib/belt/cli/routes_command/schema_loader.rb +17 -7
- data/lib/belt/cli/routes_command.rb +7 -3
- data/lib/belt/cli/setup_command.rb +1 -1
- data/lib/belt/cli/views_command.rb +96 -11
- data/lib/belt/cli.rb +6 -0
- data/lib/belt/root.rb +12 -3
- data/lib/belt/route_dsl.rb +9 -6
- data/lib/belt/version.rb +1 -1
- data/lib/belt.rb +1 -0
- data/lib/templates/frontend_infra/frontend.tf.erb +2 -1
- data/lib/templates/generate/controller.rb.erb +13 -18
- data/lib/templates/generate/model.rb.erb +1 -14
- data/lib/templates/module/frontend.tf.erb +2 -1
- data/lib/templates/module/main.tf.erb +1 -1
- data/lib/templates/new_app/AGENTS.md.erb +4 -4
- data/lib/templates/new_app/Gemfile.erb +0 -2
- data/lib/templates/new_app/README.md.erb +2 -2
- data/lib/templates/new_app/gitignore.erb +3 -0
- data/lib/templates/new_app/lambda/api.rb.erb +0 -6
- data/lib/templates/new_app/lambda/config/environment.rb.erb +0 -6
- data/lib/templates/new_app/lambda/models/application_record.rb.erb +0 -2
- data/lib/templates/plugin/AGENTS.md.erb +1 -1
- data/lib/templates/plugin/README.md.erb +1 -1
- metadata +11 -3
- /data/lib/templates/new_app/config/{schema.tf.rb.erb → contracts.rb.erb} +0 -0
- /data/lib/templates/new_app/config/{routes.tf.rb.erb → routes.rb.erb} +0 -0
|
@@ -27,7 +27,7 @@ module Belt
|
|
|
27
27
|
routes_file = find_routes_file
|
|
28
28
|
unless routes_file
|
|
29
29
|
abort 'Error: No routes file found. ' \
|
|
30
|
-
'Expected config/routes.
|
|
30
|
+
'Expected config/routes.rb (or config/routes.tf.rb, infrastructure/routes.tf.rb)'
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
dsl = load_routes(routes_file)
|
|
@@ -71,7 +71,7 @@ module Belt
|
|
|
71
71
|
@options[:output_dir] = dir
|
|
72
72
|
end
|
|
73
73
|
|
|
74
|
-
opts.on('--schema FILE', 'Path to
|
|
74
|
+
opts.on('--schema FILE', 'Path to contracts.rb for model definitions') do |file|
|
|
75
75
|
@options[:schema_file] = file
|
|
76
76
|
end
|
|
77
77
|
|
|
@@ -87,7 +87,11 @@ module Belt
|
|
|
87
87
|
end
|
|
88
88
|
|
|
89
89
|
def find_routes_file
|
|
90
|
-
candidates = [
|
|
90
|
+
candidates = [
|
|
91
|
+
'config/routes.rb',
|
|
92
|
+
'config/routes.tf.rb',
|
|
93
|
+
'infrastructure/routes.tf.rb'
|
|
94
|
+
]
|
|
91
95
|
candidates.find { |f| File.exist?(f) }
|
|
92
96
|
end
|
|
93
97
|
|
|
@@ -32,7 +32,7 @@ module Belt
|
|
|
32
32
|
puts 'Usage: belt setup <state|tables|frontend> [options]'
|
|
33
33
|
puts "\nSubcommands:"
|
|
34
34
|
puts ' state Set up S3 bucket for Terraform state'
|
|
35
|
-
puts ' tables Generate DynamoDB table definitions from
|
|
35
|
+
puts ' tables Generate DynamoDB table definitions from contracts.rb'
|
|
36
36
|
puts ' frontend Generate S3 + CloudFront infrastructure for frontend hosting'
|
|
37
37
|
exit 1
|
|
38
38
|
end
|
|
@@ -10,10 +10,14 @@ module Belt
|
|
|
10
10
|
TEMPLATE_DIR = File.expand_path('../../templates/views', __dir__)
|
|
11
11
|
|
|
12
12
|
def self.run(args)
|
|
13
|
+
force = args.delete('--force') || args.delete('-f')
|
|
14
|
+
|
|
13
15
|
name = args.shift
|
|
14
16
|
if name.nil? || name.empty?
|
|
15
|
-
puts 'Usage: belt generate views <resource> [field:type ...]'
|
|
17
|
+
puts 'Usage: belt generate views <resource> [field:type ...] [options]'
|
|
16
18
|
puts "\nGenerates React pages for all REST actions (index, show, new, edit)."
|
|
19
|
+
puts "\nOptions:"
|
|
20
|
+
puts ' --force, -f Overwrite existing files without prompting'
|
|
17
21
|
puts "\nExamples:"
|
|
18
22
|
puts ' belt generate views post title:string content:text status:string'
|
|
19
23
|
puts ' belt generate views comment body:text author:string'
|
|
@@ -25,14 +29,19 @@ module Belt
|
|
|
25
29
|
{ name: n, type: t || 'string' }
|
|
26
30
|
end
|
|
27
31
|
|
|
28
|
-
# If no fields provided, try to read from
|
|
32
|
+
# If no fields provided, try to read from contracts.rb
|
|
29
33
|
fields = read_schema_fields(name) if fields.empty?
|
|
30
34
|
|
|
31
|
-
new(name, fields).generate
|
|
35
|
+
new(name, fields, force: force).generate
|
|
32
36
|
end
|
|
33
37
|
|
|
34
38
|
def self.read_schema_fields(name)
|
|
35
|
-
schema_file = [
|
|
39
|
+
schema_file = [
|
|
40
|
+
'config/contracts.rb',
|
|
41
|
+
'config/contracts.tf.rb',
|
|
42
|
+
'config/schema.tf.rb',
|
|
43
|
+
'infrastructure/schema.tf.rb'
|
|
44
|
+
].find { |f| File.exist?(f) }
|
|
36
45
|
return [] unless schema_file
|
|
37
46
|
|
|
38
47
|
content = File.read(schema_file)
|
|
@@ -40,20 +49,37 @@ module Belt
|
|
|
40
49
|
|
|
41
50
|
# Extract fields from model block
|
|
42
51
|
if content =~ /model :#{singular} do\n(.*?)\n\s*end/m
|
|
43
|
-
::Regexp.last_match(1)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
52
|
+
block_content = ::Regexp.last_match(1)
|
|
53
|
+
timestamp_fields = %w[created_at updated_at]
|
|
54
|
+
|
|
55
|
+
# Support both formats:
|
|
56
|
+
# field :name, type: :string (legacy)
|
|
57
|
+
# string :name (current schema DSL)
|
|
58
|
+
dsl_types = %w[string text integer number boolean float date datetime]
|
|
59
|
+
dsl_pattern = /(?:#{dsl_types.join('|')}) :(\w+)/
|
|
60
|
+
fields = block_content.scan(/field :(\w+), type: :(\w+)/)
|
|
61
|
+
fields += block_content.scan(dsl_pattern).map do |match|
|
|
62
|
+
field_name = match[0]
|
|
63
|
+
# Extract type from the DSL method name on that line
|
|
64
|
+
type_match = block_content.match(/(\w+) :#{Regexp.escape(field_name)}/)
|
|
65
|
+
[field_name, type_match ? type_match[1] : 'string']
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
fields.filter_map do |n, t|
|
|
69
|
+
next if timestamp_fields.include?(n)
|
|
70
|
+
|
|
71
|
+
{ name: n, type: t }
|
|
48
72
|
end
|
|
49
73
|
else
|
|
50
74
|
[]
|
|
51
75
|
end
|
|
52
76
|
end
|
|
53
77
|
|
|
54
|
-
def initialize(name, fields)
|
|
78
|
+
def initialize(name, fields, force: false)
|
|
55
79
|
@name = name.downcase.gsub(/[^a-z0-9_]/, '_')
|
|
56
80
|
@fields = fields
|
|
81
|
+
@force = force
|
|
82
|
+
@overwrite_all = false
|
|
57
83
|
@singular_name = Belt::Inflector.singularize(@name)
|
|
58
84
|
@resource_name = Belt::Inflector.pluralize(@singular_name)
|
|
59
85
|
@class_name = Belt::Inflector.classify(@singular_name)
|
|
@@ -92,8 +118,61 @@ module Belt
|
|
|
92
118
|
def write_template(template_name, dest_path)
|
|
93
119
|
template_path = File.join(TEMPLATE_DIR, template_name)
|
|
94
120
|
content = ERB.new(File.read(template_path), trim_mode: '-').result(binding)
|
|
121
|
+
existed = File.exist?(dest_path)
|
|
122
|
+
|
|
123
|
+
if existed && !@force && !@overwrite_all
|
|
124
|
+
action = prompt_overwrite(dest_path)
|
|
125
|
+
case action
|
|
126
|
+
when :yes
|
|
127
|
+
# fall through to write
|
|
128
|
+
when :all
|
|
129
|
+
@overwrite_all = true
|
|
130
|
+
# fall through to write
|
|
131
|
+
when :no
|
|
132
|
+
puts " skip #{dest_path}"
|
|
133
|
+
return
|
|
134
|
+
when :quit
|
|
135
|
+
puts "\nAborted."
|
|
136
|
+
exit 1
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
95
140
|
File.write(dest_path, content)
|
|
96
|
-
puts " create #{dest_path}"
|
|
141
|
+
puts " #{existed ? 'overwrite' : 'create'} #{dest_path}"
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def prompt_overwrite(path)
|
|
145
|
+
return :yes if @overwrite_all
|
|
146
|
+
|
|
147
|
+
print " conflict #{path}\n"
|
|
148
|
+
print " Overwrite #{path}? (enter \"h\" for help) [Ynaqh] "
|
|
149
|
+
$stdout.flush
|
|
150
|
+
|
|
151
|
+
loop do
|
|
152
|
+
answer = $stdin.gets&.strip&.downcase
|
|
153
|
+
case answer
|
|
154
|
+
when '', 'y', 'yes'
|
|
155
|
+
return :yes
|
|
156
|
+
when 'n', 'no'
|
|
157
|
+
return :no
|
|
158
|
+
when 'a', 'all'
|
|
159
|
+
@overwrite_all = true
|
|
160
|
+
return :all
|
|
161
|
+
when 'q', 'quit'
|
|
162
|
+
return :quit
|
|
163
|
+
when 'h', 'help'
|
|
164
|
+
puts ' Y - yes, overwrite this file'
|
|
165
|
+
puts ' n - no, skip this file'
|
|
166
|
+
puts ' a - all, overwrite this and all remaining files'
|
|
167
|
+
puts ' q - quit, abort the generator'
|
|
168
|
+
puts ' h - help, show this help'
|
|
169
|
+
print " Overwrite #{path}? (enter \"h\" for help) [Ynaqh] "
|
|
170
|
+
$stdout.flush
|
|
171
|
+
else
|
|
172
|
+
print ' Please enter Y, n, a, q, or h: '
|
|
173
|
+
$stdout.flush
|
|
174
|
+
end
|
|
175
|
+
end
|
|
97
176
|
end
|
|
98
177
|
|
|
99
178
|
def inject_routes
|
|
@@ -104,6 +183,12 @@ module Belt
|
|
|
104
183
|
pages_dir = @resource_name
|
|
105
184
|
plural_class = @plural_class_name || Belt::Inflector.camelize(@resource_name)
|
|
106
185
|
|
|
186
|
+
# Skip route injection if routes for this resource already exist
|
|
187
|
+
if content.include?("path=\"/#{@resource_name}\"")
|
|
188
|
+
puts " skip #{app_jsx} (routes already exist)"
|
|
189
|
+
return
|
|
190
|
+
end
|
|
191
|
+
|
|
107
192
|
import_lines = [
|
|
108
193
|
"import #{plural_class}Index from './pages/#{pages_dir}/#{plural_class}Index'",
|
|
109
194
|
"import #{@class_name}Show from './pages/#{pages_dir}/#{@class_name}Show'",
|
data/lib/belt/cli.rb
CHANGED
|
@@ -18,9 +18,11 @@ require_relative 'cli/backup_config'
|
|
|
18
18
|
require_relative 'cli/backup_runner'
|
|
19
19
|
require_relative 'cli/server_command'
|
|
20
20
|
require_relative 'cli/routes_command'
|
|
21
|
+
require_relative 'cli/contracts_command'
|
|
21
22
|
require_relative 'cli/lambda_config_command'
|
|
22
23
|
require_relative 'cli/tasks_command'
|
|
23
24
|
require_relative 'cli/console_command'
|
|
25
|
+
require_relative 'cli/logs_command'
|
|
24
26
|
require_relative 'cli/doctor_command'
|
|
25
27
|
require_relative 'cli/plugin_command'
|
|
26
28
|
|
|
@@ -31,8 +33,10 @@ module Belt
|
|
|
31
33
|
%w[generate g] => Belt::CLI::GenerateCommand,
|
|
32
34
|
%w[destroy d] => Belt::CLI::DestroyCommand,
|
|
33
35
|
'routes' => Belt::CLI::RoutesCommand,
|
|
36
|
+
'contracts' => Belt::CLI::ContractsCommand,
|
|
34
37
|
'lambda-config' => Belt::CLI::LambdaConfigCommand,
|
|
35
38
|
%w[console c] => Belt::CLI::ConsoleCommand,
|
|
39
|
+
'logs' => Belt::CLI::LogsCommand,
|
|
36
40
|
%w[tasks --tasks -T] => Belt::CLI::TasksCommand,
|
|
37
41
|
'setup' => Belt::CLI::SetupCommand,
|
|
38
42
|
'doctor' => Belt::CLI::DoctorCommand,
|
|
@@ -109,10 +113,12 @@ module Belt
|
|
|
109
113
|
deploy frontend <env> Build and deploy frontend to AWS
|
|
110
114
|
frontend env <env> Write frontend/.env from terraform outputs
|
|
111
115
|
routes [-g PATTERN] [-f json] Show route definitions
|
|
116
|
+
contracts [-g PATTERN] [-f json] Show API request/response contracts
|
|
112
117
|
lambda-config [-e ENV] [-f json|terraform] Show merged lambda configuration
|
|
113
118
|
|
|
114
119
|
console Start an interactive console (IRB)
|
|
115
120
|
c Alias for console
|
|
121
|
+
logs [lambda] [-f] [-s 5m] [-e env] View Lambda function logs
|
|
116
122
|
tasks [-g PATTERN] [-a] List available rake tasks
|
|
117
123
|
-T [-g PATTERN] [-a] Alias for tasks
|
|
118
124
|
setup state Create/select S3 state bucket
|
data/lib/belt/root.rb
CHANGED
|
@@ -9,24 +9,32 @@ module Belt
|
|
|
9
9
|
@root = path
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
-
# Resolves the path to routes.
|
|
12
|
+
# Resolves the path to routes.rb, checking config/ first then legacy paths.
|
|
13
13
|
def self.routes_file
|
|
14
14
|
candidates = [
|
|
15
|
+
File.join(root, 'config/routes.rb'),
|
|
15
16
|
File.join(root, 'config/routes.tf.rb'),
|
|
16
17
|
File.join(root, 'infrastructure/routes.tf.rb')
|
|
17
18
|
]
|
|
18
19
|
candidates.find { |f| File.exist?(f) }
|
|
19
20
|
end
|
|
20
21
|
|
|
21
|
-
# Resolves the path to
|
|
22
|
-
def self.
|
|
22
|
+
# Resolves the path to contracts.rb, checking config/ first then legacy paths.
|
|
23
|
+
def self.contracts_file
|
|
23
24
|
candidates = [
|
|
25
|
+
File.join(root, 'config/contracts.rb'),
|
|
26
|
+
File.join(root, 'config/contracts.tf.rb'),
|
|
24
27
|
File.join(root, 'config/schema.tf.rb'),
|
|
25
28
|
File.join(root, 'infrastructure/schema.tf.rb')
|
|
26
29
|
]
|
|
27
30
|
candidates.find { |f| File.exist?(f) }
|
|
28
31
|
end
|
|
29
32
|
|
|
33
|
+
# Legacy alias for backward compatibility
|
|
34
|
+
def self.schema_file
|
|
35
|
+
contracts_file
|
|
36
|
+
end
|
|
37
|
+
|
|
30
38
|
# Resolves the lambda config directory.
|
|
31
39
|
def self.lambda_config_dir
|
|
32
40
|
File.join(root, 'config/lambda')
|
|
@@ -35,6 +43,7 @@ module Belt
|
|
|
35
43
|
def self.detect_root
|
|
36
44
|
dir = Dir.pwd
|
|
37
45
|
loop do
|
|
46
|
+
return dir if File.exist?(File.join(dir, 'config/routes.rb'))
|
|
38
47
|
return dir if File.exist?(File.join(dir, 'config/routes.tf.rb'))
|
|
39
48
|
return dir if File.exist?(File.join(dir, 'infrastructure/routes.tf.rb'))
|
|
40
49
|
|
data/lib/belt/route_dsl.rb
CHANGED
|
@@ -5,7 +5,7 @@ require_relative 'inflector'
|
|
|
5
5
|
module Belt
|
|
6
6
|
# DSL for defining API Gateway routes.
|
|
7
7
|
# Ported from terraform-provider-conveyor-belt/scripts/lib/route_dsl.rb
|
|
8
|
-
# so that `belt routes` can parse routes.
|
|
8
|
+
# so that `belt routes` can parse routes.rb without external dependencies.
|
|
9
9
|
|
|
10
10
|
class Route
|
|
11
11
|
attr_reader :method, :path, :auth, :lambda, :cors, :tables, :route_type,
|
|
@@ -518,9 +518,9 @@ module Belt
|
|
|
518
518
|
end
|
|
519
519
|
end
|
|
520
520
|
|
|
521
|
-
# SchemaBuilder captures request and response model definitions from
|
|
521
|
+
# SchemaBuilder captures request and response model definitions from contracts.rb
|
|
522
522
|
class SchemaBuilder
|
|
523
|
-
SUPPORTED_TYPES = %i[string number integer boolean array object map list].freeze
|
|
523
|
+
SUPPORTED_TYPES = %i[string text number integer boolean date datetime array object map list].freeze
|
|
524
524
|
|
|
525
525
|
attr_reader :request_models, :response_models
|
|
526
526
|
|
|
@@ -557,7 +557,7 @@ module Belt
|
|
|
557
557
|
end
|
|
558
558
|
|
|
559
559
|
class RequestModelBuilder
|
|
560
|
-
SUPPORTED_TYPES = %i[string number integer boolean array object map list].freeze
|
|
560
|
+
SUPPORTED_TYPES = %i[string text number integer boolean date datetime array object map list].freeze
|
|
561
561
|
|
|
562
562
|
attr_reader :name, :fields
|
|
563
563
|
|
|
@@ -590,6 +590,7 @@ module Belt
|
|
|
590
590
|
|
|
591
591
|
def map_type(dsl_type)
|
|
592
592
|
case dsl_type
|
|
593
|
+
when :text, :date, :datetime then 'string'
|
|
593
594
|
when :map then 'object'
|
|
594
595
|
when :list then 'array'
|
|
595
596
|
else dsl_type.to_s
|
|
@@ -598,7 +599,7 @@ module Belt
|
|
|
598
599
|
end
|
|
599
600
|
|
|
600
601
|
class ResponseModelBuilder
|
|
601
|
-
SUPPORTED_TYPES = %i[string number integer boolean array object map list].freeze
|
|
602
|
+
SUPPORTED_TYPES = %i[string text number integer boolean date datetime array object map list].freeze
|
|
602
603
|
|
|
603
604
|
attr_reader :name, :contexts, :fields
|
|
604
605
|
|
|
@@ -636,6 +637,7 @@ module Belt
|
|
|
636
637
|
|
|
637
638
|
def map_type(dsl_type)
|
|
638
639
|
case dsl_type
|
|
640
|
+
when :text, :date, :datetime then 'string'
|
|
639
641
|
when :map then 'object'
|
|
640
642
|
when :list then 'array'
|
|
641
643
|
else dsl_type.to_s
|
|
@@ -644,7 +646,7 @@ module Belt
|
|
|
644
646
|
end
|
|
645
647
|
|
|
646
648
|
class ContextBuilder
|
|
647
|
-
SUPPORTED_TYPES = %i[string number integer boolean array object map list].freeze
|
|
649
|
+
SUPPORTED_TYPES = %i[string text number integer boolean date datetime array object map list].freeze
|
|
648
650
|
|
|
649
651
|
attr_reader :name, :fields
|
|
650
652
|
|
|
@@ -673,6 +675,7 @@ module Belt
|
|
|
673
675
|
|
|
674
676
|
def map_type(dsl_type)
|
|
675
677
|
case dsl_type
|
|
678
|
+
when :text, :date, :datetime then 'string'
|
|
676
679
|
when :map then 'object'
|
|
677
680
|
when :list then 'array'
|
|
678
681
|
else dsl_type.to_s
|
data/lib/belt/version.rb
CHANGED
data/lib/belt.rb
CHANGED
|
@@ -9,7 +9,8 @@ resource "random_string" "frontend_suffix" {
|
|
|
9
9
|
|
|
10
10
|
# S3 bucket for frontend static assets
|
|
11
11
|
resource "aws_s3_bucket" "frontend" {
|
|
12
|
-
bucket
|
|
12
|
+
bucket = "<%= s3_safe_name(@app_name) %>-frontend-${var.environment}-${random_string.frontend_suffix.result}"
|
|
13
|
+
force_destroy = true
|
|
13
14
|
|
|
14
15
|
lifecycle {
|
|
15
16
|
ignore_changes = [bucket]
|
|
@@ -6,53 +6,48 @@ module <%= @module_name %>Controllers
|
|
|
6
6
|
class <%= @class_name %>sController < ApplicationController
|
|
7
7
|
# GET /<%= @resource_name %>
|
|
8
8
|
def index
|
|
9
|
-
|
|
10
|
-
success_response(<%= @resource_name %>: <%= @resource_name %>.map(&:to_h))
|
|
9
|
+
@<%= @resource_name %> = <%= @class_name %>.all
|
|
11
10
|
end
|
|
12
11
|
|
|
13
12
|
# POST /<%= @resource_name %>
|
|
14
13
|
def create
|
|
15
|
-
|
|
14
|
+
@<%= @singular_name %> = <%= @class_name %>.new(<%= @fields.map { |f| "#{f[:name]}: params[:#{f[:name]}]" }.join(', ') %>)
|
|
16
15
|
|
|
17
|
-
if
|
|
18
|
-
|
|
16
|
+
if @<%= @singular_name %>.save
|
|
17
|
+
response_status :created
|
|
19
18
|
else
|
|
20
|
-
error_response(
|
|
19
|
+
error_response(@<%= @singular_name %>.errors.full_messages.join(', '), :unprocessable_entity)
|
|
21
20
|
end
|
|
22
21
|
end
|
|
23
22
|
|
|
24
23
|
# GET /<%= @resource_name %>/:<%= @singular_name %>_id
|
|
25
24
|
def show
|
|
26
|
-
|
|
27
|
-
return error_response('<%= @class_name %> not found',
|
|
28
|
-
|
|
29
|
-
success_response(<%= @singular_name %>: <%= @singular_name %>.to_h)
|
|
25
|
+
@<%= @singular_name %> = <%= @class_name %>.find(params[:<%= @singular_name %>_id])
|
|
26
|
+
return error_response('<%= @class_name %> not found', :not_found) unless @<%= @singular_name %>
|
|
30
27
|
end
|
|
31
28
|
|
|
32
29
|
# PUT /<%= @resource_name %>/:<%= @singular_name %>_id
|
|
33
30
|
def update
|
|
34
|
-
|
|
35
|
-
return error_response('<%= @class_name %> not found',
|
|
31
|
+
@<%= @singular_name %> = <%= @class_name %>.find(params[:<%= @singular_name %>_id])
|
|
32
|
+
return error_response('<%= @class_name %> not found', :not_found) unless @<%= @singular_name %>
|
|
36
33
|
|
|
37
34
|
attrs = {}
|
|
38
35
|
<% @fields.each do |field| -%>
|
|
39
36
|
attrs[:<%= field[:name] %>] = params[:<%= field[:name] %>] if params.key?(:<%= field[:name] %>)
|
|
40
37
|
<% end -%>
|
|
41
38
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
else
|
|
45
|
-
error_response(<%= @singular_name %>.errors.full_messages.join(', '), 422)
|
|
39
|
+
unless @<%= @singular_name %>.update(attrs)
|
|
40
|
+
error_response(@<%= @singular_name %>.errors.full_messages.join(', '), :unprocessable_entity)
|
|
46
41
|
end
|
|
47
42
|
end
|
|
48
43
|
|
|
49
44
|
# DELETE /<%= @resource_name %>/:<%= @singular_name %>_id
|
|
50
45
|
def destroy
|
|
51
46
|
<%= @singular_name %> = <%= @class_name %>.find(params[:<%= @singular_name %>_id])
|
|
52
|
-
return error_response('<%= @class_name %> not found',
|
|
47
|
+
return error_response('<%= @class_name %> not found', :not_found) unless <%= @singular_name %>
|
|
53
48
|
|
|
54
49
|
<%= @singular_name %>.destroy
|
|
55
|
-
|
|
50
|
+
head :no_content
|
|
56
51
|
end
|
|
57
52
|
end
|
|
58
53
|
end
|
|
@@ -1,18 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
class <%= @class_name %> < ApplicationRecord
|
|
4
|
-
|
|
5
|
-
attr_accessor :<%= field[:name] %>
|
|
6
|
-
<% end -%>
|
|
7
|
-
|
|
8
|
-
def to_h
|
|
9
|
-
{
|
|
10
|
-
id: id,
|
|
11
|
-
<% @fields.each do |field| -%>
|
|
12
|
-
<%= field[:name] %>: <%= field[:name] %>,
|
|
13
|
-
<% end -%>
|
|
14
|
-
created_at: created_at,
|
|
15
|
-
updated_at: updated_at
|
|
16
|
-
}
|
|
17
|
-
end
|
|
4
|
+
attr_accessor <%= @fields.map { |f| ":#{f[:name]}" }.join(', ') %>
|
|
18
5
|
end
|
|
@@ -9,7 +9,8 @@ resource "random_string" "frontend_suffix" {
|
|
|
9
9
|
|
|
10
10
|
# S3 bucket for frontend static assets
|
|
11
11
|
resource "aws_s3_bucket" "frontend" {
|
|
12
|
-
bucket
|
|
12
|
+
bucket = "${var.app_name}-frontend-${var.environment}-${random_string.frontend_suffix.result}"
|
|
13
|
+
force_destroy = true
|
|
13
14
|
|
|
14
15
|
lifecycle {
|
|
15
16
|
ignore_changes = [bucket]
|
|
@@ -22,7 +22,7 @@ resource "conveyor_belt" "main" {
|
|
|
22
22
|
provider = conveyor-belt
|
|
23
23
|
|
|
24
24
|
# path.module is infrastructure/modules/app — climb three levels to project root
|
|
25
|
-
source = "${path.module}/../../../config/routes.
|
|
25
|
+
source = "${path.module}/../../../config/routes.rb"
|
|
26
26
|
app_name = var.app_name
|
|
27
27
|
lambda_source_dir = "${path.module}/../../../lambda"
|
|
28
28
|
lambda_shared_dirs = ["controllers", "helpers", "lib", "models", "views"]
|
|
@@ -6,7 +6,7 @@ This file explains the project structure, tooling, and conventions for AI agents
|
|
|
6
6
|
|
|
7
7
|
- **Belt** — CLI and runtime framework (like Rails for serverless). Provides Lambda handler, action router, controller base class, and CLI tooling.
|
|
8
8
|
- **ActiveItem** — ActiveRecord-like ORM for DynamoDB. Models inherit from `ActiveItem::Base`.
|
|
9
|
-
- **Conveyor Belt** — Terraform provider that reads a Ruby DSL (`routes.
|
|
9
|
+
- **Conveyor Belt** — Terraform provider that reads a Ruby DSL (`routes.rb`) and creates API Gateway + Lambda + IAM infrastructure.
|
|
10
10
|
- **Lambda Loadout** — Lambda cold-start optimizer (auto-required by Belt).
|
|
11
11
|
|
|
12
12
|
## Project Structure
|
|
@@ -23,8 +23,8 @@ This file explains the project structure, tooling, and conventions for AI agents
|
|
|
23
23
|
│ ├── lib/routes/ # Route manifests (auto-generated)
|
|
24
24
|
│ └── Gemfile # Lambda-specific dependencies
|
|
25
25
|
├── config/
|
|
26
|
-
│ ├── routes.
|
|
27
|
-
│ ├──
|
|
26
|
+
│ ├── routes.rb # API routes (Conveyor Belt DSL)
|
|
27
|
+
│ ├── contracts.rb # API request/response contracts
|
|
28
28
|
│ └── lambda/ # Per-lambda config (like database.yml)
|
|
29
29
|
│ └── api.yml # Lambda timeout, memory, env vars
|
|
30
30
|
├── infrastructure/
|
|
@@ -53,7 +53,7 @@ belt output <env> # terraform output
|
|
|
53
53
|
|
|
54
54
|
## How Routing Works
|
|
55
55
|
|
|
56
|
-
1. `config/routes.
|
|
56
|
+
1. `config/routes.rb` defines routes using a DSL:
|
|
57
57
|
```ruby
|
|
58
58
|
Belt.application.routes.draw do
|
|
59
59
|
namespace :<%= @app_name %> do
|
|
@@ -6,8 +6,8 @@ A serverless application built with [Belt](https://github.com/stowzilla/belt) an
|
|
|
6
6
|
|
|
7
7
|
```
|
|
8
8
|
├── config/
|
|
9
|
-
│ ├── routes.
|
|
10
|
-
│ ├──
|
|
9
|
+
│ ├── routes.rb # API route definitions
|
|
10
|
+
│ ├── contracts.rb # API request/response contracts
|
|
11
11
|
│ └── lambda/ # Per-lambda config (timeout, memory, env vars)
|
|
12
12
|
│ └── api.yml
|
|
13
13
|
├── infrastructure/ # Terraform environments
|
|
@@ -1,15 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'belt'
|
|
4
|
-
require 'activeitem'
|
|
5
4
|
|
|
6
5
|
include Belt::LambdaHandler
|
|
7
6
|
|
|
8
|
-
ActiveItem.configure do |config|
|
|
9
|
-
config.table_prefix = ENV['APP_NAME']
|
|
10
|
-
config.environment = ENV['ENVIRONMENT']
|
|
11
|
-
end
|
|
12
|
-
|
|
13
7
|
require_relative 'lib/routes/api_routes'
|
|
14
8
|
<% @resources&.each do |r| -%>
|
|
15
9
|
require_relative 'controllers/<%= @app_name %>/<%= r %>_controller'
|
|
@@ -3,16 +3,10 @@
|
|
|
3
3
|
# Boot the application. Used by `belt console` and Lambda at runtime.
|
|
4
4
|
|
|
5
5
|
require 'belt'
|
|
6
|
-
require 'activeitem'
|
|
7
6
|
|
|
8
7
|
ENV['APP_NAME'] ||= '<%= @app_name %>'
|
|
9
8
|
ENV['AWS_REGION'] ||= 'us-east-1'
|
|
10
9
|
|
|
11
|
-
ActiveItem.configure do |config|
|
|
12
|
-
config.table_prefix = ENV['APP_NAME']
|
|
13
|
-
config.environment = ENV['ENVIRONMENT']
|
|
14
|
-
end
|
|
15
|
-
|
|
16
10
|
# Load lib and models
|
|
17
11
|
Dir[File.join(__dir__, '..', 'lib', '**', '*.rb')].sort.each { |f| require f }
|
|
18
12
|
|
|
@@ -67,7 +67,7 @@ When fleshing out the generator, typically install:
|
|
|
67
67
|
1. **Terraform module** → `infrastructure/modules/<%= plugin_name %>/` (`main.tf`, `variables.tf`, `outputs.tf`)
|
|
68
68
|
2. **Lambda config** → `config/lambda/<%= plugin_name %>.yml` (timeout, memory, env, triggers)
|
|
69
69
|
3. **Lambda entrypoint** → `lambda/<%= plugin_name %>.rb` using `Belt::LambdaHandler`
|
|
70
|
-
4. **Routes / schema** → inject into `config/routes.
|
|
70
|
+
4. **Routes / schema** → inject into `config/routes.rb` or `config/contracts.rb` when needed
|
|
71
71
|
5. **Optional overrides** → `--controllers` (or similar) for app-local subclasses — keep defaults in the gem
|
|
72
72
|
6. **Destroy path** → `belt destroy <%= plugin_name %>` removes generated artifacts
|
|
73
73
|
7. **Help** → `.description` plus `--help` / `-h` explaining files and next steps
|
|
@@ -42,7 +42,7 @@ Typical plugin generators create some combination of:
|
|
|
42
42
|
- **Terraform module** — `infrastructure/modules/<%= plugin_name %>/`
|
|
43
43
|
- **Lambda entry point** — `lambda/<%= plugin_name %>.rb`
|
|
44
44
|
- **Lambda config** — `config/lambda/<%= plugin_name %>.yml`
|
|
45
|
-
- **Route / schema injection** — updates to `routes.
|
|
45
|
+
- **Route / schema injection** — updates to `config/routes.rb` / `config/contracts.rb`
|
|
46
46
|
- **Optional controller overrides** — `belt g <%= plugin_name %> --controllers`
|
|
47
47
|
|
|
48
48
|
## Development
|