belt 0.1.7 → 0.1.9
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/lib/belt/cli/deploy_command.rb +40 -0
- data/lib/belt/cli/destroy_command.rb +21 -8
- data/lib/belt/cli/environment_command.rb +4 -12
- data/lib/belt/cli/frontend_command.rb +11 -13
- data/lib/belt/cli/frontend_setup_command.rb +14 -25
- data/lib/belt/cli/generate_command.rb +106 -32
- data/lib/belt/cli/new_command.rb +82 -35
- data/lib/belt/cli/routes_command.rb +1 -9
- data/lib/belt/cli/setup_command.rb +2 -8
- data/lib/belt/cli/tables_command.rb +58 -25
- data/lib/belt/cli/views_command.rb +11 -8
- data/lib/belt/inflector.rb +58 -0
- data/lib/belt/lambda_handler.rb +11 -0
- data/lib/belt/route_dsl.rb +3 -11
- data/lib/belt/table_inference.rb +4 -18
- data/lib/belt/version.rb +1 -1
- data/lib/templates/environment/backend.tf.erb +2 -2
- data/lib/templates/environment/main.tf.erb +14 -18
- data/lib/templates/environment/outputs.tf.erb +28 -3
- data/lib/templates/environment/terraform.tfvars.erb +5 -0
- data/lib/templates/environment/variables.tf.erb +6 -0
- data/lib/templates/generate/controller.rb.erb +0 -1
- data/lib/templates/generate/model.rb.erb +0 -2
- data/lib/templates/module/dns.tf.erb +83 -0
- data/lib/templates/module/frontend.tf.erb +179 -0
- data/lib/templates/module/main.tf.erb +42 -0
- data/lib/templates/module/outputs.tf.erb +24 -0
- data/lib/templates/module/variables.tf.erb +27 -0
- data/lib/templates/new_app/lambda/config/environment.rb.erb +7 -1
- data/lib/templates/views/Form.jsx.erb +63 -7
- data/lib/templates/views/Index.jsx.erb +41 -10
- data/lib/templates/views/Show.jsx.erb +12 -2
- metadata +21 -2
- data/lib/templates/new_app/lambda/models/concerns/timestampable.rb.erb +0 -23
data/lib/belt/cli/new_command.rb
CHANGED
|
@@ -17,6 +17,7 @@ module Belt
|
|
|
17
17
|
frontend = nil
|
|
18
18
|
bucket = nil
|
|
19
19
|
environments = nil
|
|
20
|
+
domain = nil
|
|
20
21
|
|
|
21
22
|
i = 0
|
|
22
23
|
while i < args.length
|
|
@@ -43,6 +44,11 @@ module Belt
|
|
|
43
44
|
environments = args[i]
|
|
44
45
|
when /^--environments=/
|
|
45
46
|
environments = arg.split('=', 2).last
|
|
47
|
+
when '--domain'
|
|
48
|
+
i += 1
|
|
49
|
+
domain = args[i]
|
|
50
|
+
when /^--domain=/
|
|
51
|
+
domain = arg.split('=', 2).last
|
|
46
52
|
else
|
|
47
53
|
app_name ||= arg unless arg.start_with?('-')
|
|
48
54
|
end
|
|
@@ -54,6 +60,7 @@ module Belt
|
|
|
54
60
|
puts ''
|
|
55
61
|
puts 'Options:'
|
|
56
62
|
puts ' --frontend react|vue|svelte Set up frontend framework'
|
|
63
|
+
puts ' --domain DOMAIN Custom domain (e.g., myapp.com)'
|
|
57
64
|
puts ' --bucket BUCKET_NAME S3 bucket for Terraform state'
|
|
58
65
|
puts ' --state-bucket BUCKET_NAME Alias for --bucket'
|
|
59
66
|
puts ' --environments dev,prod Comma-separated environments (default: dev,prod)'
|
|
@@ -61,14 +68,15 @@ module Belt
|
|
|
61
68
|
exit 1
|
|
62
69
|
end
|
|
63
70
|
|
|
64
|
-
new(app_name, frontend: frontend, bucket: bucket, environments: environments).generate
|
|
71
|
+
new(app_name, frontend: frontend, bucket: bucket, environments: environments, domain: domain).generate
|
|
65
72
|
end
|
|
66
73
|
|
|
67
|
-
def initialize(app_name, frontend: nil, bucket: nil, environments: nil)
|
|
74
|
+
def initialize(app_name, frontend: nil, bucket: nil, environments: nil, domain: nil)
|
|
68
75
|
@app_name = app_name.gsub(/[^a-z0-9_-]/i, '_').downcase
|
|
69
76
|
@module_name = @app_name.split(/[-_]/).map(&:capitalize).join
|
|
70
77
|
@frontend = frontend
|
|
71
78
|
@bucket = bucket
|
|
79
|
+
@domain = domain
|
|
72
80
|
@environments = parse_environments(environments)
|
|
73
81
|
@resolved_bucket = nil
|
|
74
82
|
@state_setup_succeeded = false
|
|
@@ -82,6 +90,7 @@ module Belt
|
|
|
82
90
|
|
|
83
91
|
puts "Creating new Belt application: #{@app_name}"
|
|
84
92
|
create_structure
|
|
93
|
+
generate_module
|
|
85
94
|
generate_environments
|
|
86
95
|
generate_frontend if @frontend
|
|
87
96
|
init_git
|
|
@@ -89,6 +98,7 @@ module Belt
|
|
|
89
98
|
setup_state
|
|
90
99
|
puts "\n✓ #{@app_name} created successfully!"
|
|
91
100
|
print_next_steps
|
|
101
|
+
enter_project!
|
|
92
102
|
end
|
|
93
103
|
|
|
94
104
|
private
|
|
@@ -109,11 +119,10 @@ module Belt
|
|
|
109
119
|
%W[
|
|
110
120
|
#{@app_name}/lambda/controllers/api
|
|
111
121
|
#{@app_name}/lambda/models
|
|
112
|
-
#{@app_name}/lambda/models/concerns
|
|
113
122
|
#{@app_name}/lambda/lib/routes
|
|
114
123
|
#{@app_name}/lambda/config
|
|
115
124
|
#{@app_name}/lambda/spec
|
|
116
|
-
#{@app_name}/infrastructure
|
|
125
|
+
#{@app_name}/infrastructure/modules/app
|
|
117
126
|
]
|
|
118
127
|
end
|
|
119
128
|
|
|
@@ -124,7 +133,6 @@ module Belt
|
|
|
124
133
|
'lambda/api.rb.erb' => "#{@app_name}/lambda/api.rb",
|
|
125
134
|
'lambda/config/environment.rb.erb' => "#{@app_name}/lambda/config/environment.rb",
|
|
126
135
|
'lambda/models/application_record.rb.erb' => "#{@app_name}/lambda/models/application_record.rb",
|
|
127
|
-
'lambda/models/concerns/timestampable.rb.erb' => "#{@app_name}/lambda/models/concerns/timestampable.rb",
|
|
128
136
|
'lambda/controllers/application_controller.rb.erb' =>
|
|
129
137
|
"#{@app_name}/lambda/controllers/api/application_controller.rb",
|
|
130
138
|
'lambda/lib/routes/routes.rb.erb' => "#{@app_name}/lambda/lib/routes/api_routes.rb",
|
|
@@ -136,12 +144,32 @@ module Belt
|
|
|
136
144
|
}
|
|
137
145
|
end
|
|
138
146
|
|
|
147
|
+
def generate_module
|
|
148
|
+
module_dir = "#{@app_name}/infrastructure/modules/app"
|
|
149
|
+
module_template_dir = File.expand_path('../../templates/module', File.dirname(__FILE__))
|
|
150
|
+
|
|
151
|
+
module_templates = {
|
|
152
|
+
'main.tf.erb' => 'main.tf',
|
|
153
|
+
'variables.tf.erb' => 'variables.tf',
|
|
154
|
+
'outputs.tf.erb' => 'outputs.tf',
|
|
155
|
+
'dns.tf.erb' => 'dns.tf'
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
module_templates.each do |template_name, dest_file|
|
|
159
|
+
dest_path = File.join(module_dir, dest_file)
|
|
160
|
+
template_path = File.join(module_template_dir, template_name)
|
|
161
|
+
content = ERB.new(File.read(template_path), trim_mode: '-').result(binding)
|
|
162
|
+
File.write(dest_path, content)
|
|
163
|
+
puts " create #{dest_path}"
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
139
167
|
def generate_environments
|
|
140
168
|
return if @environments.empty?
|
|
141
169
|
|
|
142
170
|
Dir.chdir(@app_name) do
|
|
143
171
|
@environments.each do |env_name|
|
|
144
|
-
Belt::CLI::EnvironmentCommand.new(env_name, quiet: true).generate
|
|
172
|
+
Belt::CLI::EnvironmentCommand.new(env_name, quiet: true, domain: @domain).generate
|
|
145
173
|
end
|
|
146
174
|
end
|
|
147
175
|
end
|
|
@@ -150,28 +178,11 @@ module Belt
|
|
|
150
178
|
return if @environments.empty?
|
|
151
179
|
|
|
152
180
|
Dir.chdir(@app_name) do
|
|
153
|
-
#
|
|
154
|
-
|
|
155
|
-
@resolved_bucket = @bucket
|
|
156
|
-
elsif aws_configured?
|
|
157
|
-
region = detect_region
|
|
158
|
-
@resolved_bucket = s3_safe_name("#{@app_name}-terraform-state-#{@aws_account_id}-#{region}")
|
|
159
|
-
else
|
|
160
|
-
@resolved_bucket = s3_safe_name("#{@app_name}-terraform-state")
|
|
161
|
-
end
|
|
162
|
-
|
|
163
|
-
# Update backend.tf files with the resolved bucket name
|
|
164
|
-
@environments.each do |env_name|
|
|
165
|
-
backend_file = "infrastructure/#{env_name}/backend.tf"
|
|
166
|
-
next unless File.exist?(backend_file)
|
|
167
|
-
|
|
168
|
-
content = File.read(backend_file)
|
|
169
|
-
updated = content.gsub(/bucket\s*=\s*"[^"]+"/, "bucket = \"#{@resolved_bucket}\"")
|
|
170
|
-
File.write(backend_file, updated) if updated != content
|
|
171
|
-
end
|
|
181
|
+
# Shared bucket: one per AWS account, all belt apps share it
|
|
182
|
+
@resolved_bucket = @bucket || 'belt-terraform-state'
|
|
172
183
|
|
|
173
184
|
# Attempt to actually create the bucket if credentials are available
|
|
174
|
-
if
|
|
185
|
+
if aws_configured?
|
|
175
186
|
puts "\n Setting up Terraform state bucket..."
|
|
176
187
|
begin
|
|
177
188
|
Belt::CLI::SetupCommand.new(["--bucket", @resolved_bucket]).run_state_setup
|
|
@@ -182,6 +193,7 @@ module Belt
|
|
|
182
193
|
end
|
|
183
194
|
else
|
|
184
195
|
puts "\n State bucket: #{@resolved_bucket}"
|
|
196
|
+
puts " State keys: #{s3_safe_name(@app_name)}/<env>/terraform.tfstate"
|
|
185
197
|
if @aws_error&.include?('ForbiddenException') || @aws_error&.include?('AccessDenied')
|
|
186
198
|
puts " ⚠ AWS credentials found but access denied — check your profile/role configuration."
|
|
187
199
|
puts " #{@aws_error}" if @aws_error
|
|
@@ -194,14 +206,6 @@ module Belt
|
|
|
194
206
|
end
|
|
195
207
|
end
|
|
196
208
|
|
|
197
|
-
def detect_region
|
|
198
|
-
Dir.glob('infrastructure/*/backend.tf').each do |f|
|
|
199
|
-
match = File.read(f).match(/region\s*=\s*"([^"]+)"/)
|
|
200
|
-
return match[1] if match
|
|
201
|
-
end
|
|
202
|
-
'us-east-1'
|
|
203
|
-
end
|
|
204
|
-
|
|
205
209
|
def aws_configured?
|
|
206
210
|
output, status = Open3.capture2e('aws', 'sts', 'get-caller-identity')
|
|
207
211
|
if status.success?
|
|
@@ -253,13 +257,56 @@ module Belt
|
|
|
253
257
|
|
|
254
258
|
def print_next_steps
|
|
255
259
|
puts "\nNext steps:"
|
|
256
|
-
puts " cd #{@app_name}"
|
|
257
260
|
unless @state_setup_succeeded
|
|
258
261
|
puts ' # Configure AWS credentials (aws sso login / AWS_PROFILE)'
|
|
259
262
|
puts ' belt setup state # Create the S3 state bucket'
|
|
260
263
|
end
|
|
261
264
|
puts ' belt deploy # Deploy to AWS'
|
|
262
265
|
puts ' belt server # Start local frontend server' if @frontend
|
|
266
|
+
if @domain
|
|
267
|
+
puts "\n Custom domain: #{@domain}"
|
|
268
|
+
puts " prod → #{@domain}"
|
|
269
|
+
puts " dev → dev.#{@domain}"
|
|
270
|
+
puts ''
|
|
271
|
+
puts ' DNS setup (after first deploy):'
|
|
272
|
+
puts ' ─────────────────────────────────────────────────────────────────'
|
|
273
|
+
puts ' 1. Run: terraform output name_servers'
|
|
274
|
+
puts ' This prints the 4 NS records for your Route53 hosted zone.'
|
|
275
|
+
puts ''
|
|
276
|
+
puts ' 2. Point your domain to those nameservers:'
|
|
277
|
+
puts ''
|
|
278
|
+
puts ' • Domain registered OUTSIDE AWS (GoDaddy, Namecheap, etc.):'
|
|
279
|
+
puts ' Go to your registrar → DNS/Nameserver settings → replace the'
|
|
280
|
+
puts ' default nameservers with the 4 values from step 1.'
|
|
281
|
+
puts ''
|
|
282
|
+
puts ' • Domain registered IN AWS (Route53 Registered Domains):'
|
|
283
|
+
puts ' Go to Route53 → Registered Domains → your domain → Name servers'
|
|
284
|
+
puts ' → Edit → paste the 4 values from step 1.'
|
|
285
|
+
puts ''
|
|
286
|
+
puts ' 3. Wait for propagation (usually 5–30 min, can take up to 48h).'
|
|
287
|
+
puts ' Verify: dig +short NS #{@domain}'
|
|
288
|
+
puts ' ─────────────────────────────────────────────────────────────────'
|
|
289
|
+
else
|
|
290
|
+
puts "\n To add a custom domain later, set `domain` in infrastructure/<env>/terraform.tfvars:"
|
|
291
|
+
puts ' domain = "myapp.com"'
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def enter_project!
|
|
296
|
+
project_path = File.expand_path(@app_name)
|
|
297
|
+
|
|
298
|
+
# Don't exec into a subshell if:
|
|
299
|
+
# - Not running interactively (CI, scripts, piped)
|
|
300
|
+
# - User explicitly opted out
|
|
301
|
+
unless $stdin.tty? && ENV['BELT_NO_CD'].nil?
|
|
302
|
+
puts "\n cd #{@app_name}"
|
|
303
|
+
return
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
Dir.chdir(project_path)
|
|
307
|
+
shell = ENV['SHELL'] || '/bin/bash'
|
|
308
|
+
puts "\n Entering #{@app_name}/...\n\n"
|
|
309
|
+
exec(shell)
|
|
263
310
|
end
|
|
264
311
|
|
|
265
312
|
def s3_safe_name(name)
|
|
@@ -292,15 +292,7 @@ module Belt
|
|
|
292
292
|
end
|
|
293
293
|
|
|
294
294
|
def singularize(word)
|
|
295
|
-
|
|
296
|
-
"#{word[0..-4]}y"
|
|
297
|
-
elsif word.end_with?('ses') || word.end_with?('xes') || word.end_with?('zes')
|
|
298
|
-
word[0..-3]
|
|
299
|
-
elsif word.end_with?('s') && !word.end_with?('ss')
|
|
300
|
-
word[0..-2]
|
|
301
|
-
else
|
|
302
|
-
word
|
|
303
|
-
end
|
|
295
|
+
Belt::Inflector.singularize(word)
|
|
304
296
|
end
|
|
305
297
|
end
|
|
306
298
|
end
|
|
@@ -61,7 +61,7 @@ module Belt
|
|
|
61
61
|
exit 1
|
|
62
62
|
end
|
|
63
63
|
|
|
64
|
-
#
|
|
64
|
+
# Resolve final bucket name now that we have credentials
|
|
65
65
|
@bucket_name = resolve_bucket_name unless @custom_bucket
|
|
66
66
|
|
|
67
67
|
@bucket_name = interactive_bucket_selection if @select_mode
|
|
@@ -142,13 +142,7 @@ module Belt
|
|
|
142
142
|
if @custom_bucket
|
|
143
143
|
@custom_bucket
|
|
144
144
|
else
|
|
145
|
-
|
|
146
|
-
"#{@app_name}-terraform-state-#{@env_name}"
|
|
147
|
-
else
|
|
148
|
-
"#{@app_name}-terraform-state"
|
|
149
|
-
end
|
|
150
|
-
base = "#{base}-#{@aws_account_id}-#{@region}" if @aws_account_id
|
|
151
|
-
s3_safe_name(base)
|
|
145
|
+
'belt-terraform-state'
|
|
152
146
|
end
|
|
153
147
|
end
|
|
154
148
|
|
|
@@ -2,11 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative 'app_detection'
|
|
4
4
|
require_relative 'env_resolver'
|
|
5
|
+
require_relative 'terraform_command'
|
|
6
|
+
require_relative '../inflector'
|
|
5
7
|
|
|
6
8
|
module Belt
|
|
7
9
|
module CLI
|
|
8
10
|
class TablesCommand
|
|
9
11
|
SCHEMA_FILE = 'infrastructure/schema.tf.rb'
|
|
12
|
+
MODULE_DIR = 'infrastructure/modules/app'
|
|
10
13
|
|
|
11
14
|
include AppDetection
|
|
12
15
|
|
|
@@ -14,31 +17,40 @@ module Belt
|
|
|
14
17
|
env = EnvResolver.resolve(args)
|
|
15
18
|
|
|
16
19
|
if env.nil?
|
|
17
|
-
puts 'Usage: belt setup tables
|
|
18
|
-
puts "\nReads schema.tf.rb and generates dynamodb.tf in the
|
|
20
|
+
puts 'Usage: belt setup tables [environment]'
|
|
21
|
+
puts "\nReads schema.tf.rb and generates dynamodb.tf in the app module."
|
|
19
22
|
puts 'You can also set BELT_ENV to skip the environment argument.'
|
|
20
23
|
puts "\nExamples:"
|
|
21
|
-
puts ' belt setup tables
|
|
22
|
-
puts ' belt setup tables
|
|
23
|
-
puts ' BELT_ENV=
|
|
24
|
+
puts ' belt setup tables'
|
|
25
|
+
puts ' belt setup tables dev'
|
|
26
|
+
puts ' BELT_ENV=dev belt setup tables'
|
|
24
27
|
exit 1
|
|
25
28
|
end
|
|
26
29
|
|
|
27
30
|
new(env).run
|
|
28
31
|
end
|
|
29
32
|
|
|
30
|
-
|
|
33
|
+
# Automatically sync dynamodb.tf in the app module.
|
|
34
|
+
# Called by generators after updating schema.tf.rb.
|
|
35
|
+
def self.sync_all_environments
|
|
36
|
+
return unless File.exist?(SCHEMA_FILE)
|
|
37
|
+
|
|
38
|
+
# With the module approach, we only need to generate once into modules/app/
|
|
39
|
+
new(nil, quiet: true).run
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def initialize(env, quiet: false)
|
|
31
43
|
@env = env
|
|
44
|
+
@quiet = quiet
|
|
32
45
|
@app_name = detect_app_name
|
|
33
|
-
@env_dir = "infrastructure/#{@env}"
|
|
34
46
|
end
|
|
35
47
|
|
|
36
48
|
def run
|
|
37
49
|
validate!
|
|
38
50
|
models = parse_schema
|
|
39
51
|
if models.empty?
|
|
40
|
-
puts "No models found in #{SCHEMA_FILE}"
|
|
41
|
-
|
|
52
|
+
puts "No models found in #{SCHEMA_FILE}" unless @quiet
|
|
53
|
+
return
|
|
42
54
|
end
|
|
43
55
|
|
|
44
56
|
generate_dynamodb_tf(models)
|
|
@@ -47,44 +59,65 @@ module Belt
|
|
|
47
59
|
private
|
|
48
60
|
|
|
49
61
|
def validate!
|
|
50
|
-
|
|
51
|
-
|
|
62
|
+
unless File.exist?(SCHEMA_FILE)
|
|
63
|
+
abort "Error: #{SCHEMA_FILE} not found. Run `belt generate resource` first." unless @quiet
|
|
64
|
+
return
|
|
65
|
+
end
|
|
66
|
+
return if Dir.exist?(MODULE_DIR)
|
|
52
67
|
|
|
53
|
-
|
|
54
|
-
|
|
68
|
+
unless @quiet
|
|
69
|
+
abort "Error: Module directory not found at #{MODULE_DIR}/.\n" \
|
|
70
|
+
"Run `belt new` to create a project with the correct structure."
|
|
71
|
+
end
|
|
55
72
|
end
|
|
56
73
|
|
|
57
74
|
def parse_schema
|
|
75
|
+
return [] unless File.exist?(SCHEMA_FILE)
|
|
76
|
+
|
|
58
77
|
parser = SchemaParser.new
|
|
59
78
|
schema_content = File.read(SCHEMA_FILE)
|
|
60
79
|
|
|
61
80
|
# Replace DSL wrapper with direct parser call
|
|
62
|
-
|
|
63
|
-
inner
|
|
81
|
+
inner = schema_content.sub(/\A(?:Belt\.application)\.schema\.draw do\n?/, '').sub(/\n?end\s*\z/, '')
|
|
82
|
+
inner.strip!
|
|
83
|
+
|
|
84
|
+
return [] if inner.empty? || inner.match?(/\A\s*\z/m)
|
|
85
|
+
|
|
64
86
|
parser.instance_eval(inner, SCHEMA_FILE)
|
|
65
87
|
parser.models
|
|
66
88
|
end
|
|
67
89
|
|
|
68
90
|
def generate_dynamodb_tf(models)
|
|
69
|
-
dest = File.join(
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
91
|
+
dest = File.join(MODULE_DIR, 'dynamodb.tf')
|
|
92
|
+
existing_content = File.exist?(dest) ? File.read(dest) : nil
|
|
93
|
+
new_content = render_dynamodb(models)
|
|
94
|
+
|
|
95
|
+
# Skip if content is unchanged
|
|
96
|
+
return if existing_content == new_content
|
|
97
|
+
|
|
98
|
+
File.write(dest, new_content)
|
|
99
|
+
|
|
100
|
+
if @quiet
|
|
101
|
+
verb = existing_content ? 'update' : 'create'
|
|
102
|
+
puts " #{verb} #{dest}"
|
|
103
|
+
else
|
|
104
|
+
puts " create #{dest}"
|
|
105
|
+
puts "\n✓ Generated DynamoDB tables for #{models.size} model(s):"
|
|
106
|
+
models.each { |m| puts " • #{Belt::Inflector.pluralize(m[:name])}" }
|
|
107
|
+
puts "\nRun `belt deploy` to create them."
|
|
108
|
+
end
|
|
76
109
|
end
|
|
77
110
|
|
|
78
111
|
def render_dynamodb(models)
|
|
79
112
|
blocks = models.map { |m| render_table(m) }
|
|
80
113
|
"# Auto-generated by Belt from schema.tf.rb\n" \
|
|
81
|
-
"# Do not edit manually — re-run `belt setup tables
|
|
114
|
+
"# Do not edit manually — re-run `belt setup tables`\n\n#{blocks.join("\n\n")}\n"
|
|
82
115
|
end
|
|
83
116
|
|
|
84
117
|
def render_table(model)
|
|
85
118
|
name = table_name(model[:name])
|
|
86
119
|
<<~HCL
|
|
87
|
-
resource "aws_dynamodb_table" "#{model[:name]}
|
|
120
|
+
resource "aws_dynamodb_table" "#{Belt::Inflector.pluralize(model[:name])}" {
|
|
88
121
|
name = "#{name}"
|
|
89
122
|
billing_mode = "PAY_PER_REQUEST"
|
|
90
123
|
hash_key = "id"
|
|
@@ -104,7 +137,7 @@ module Belt
|
|
|
104
137
|
end
|
|
105
138
|
|
|
106
139
|
def table_name(model_name)
|
|
107
|
-
"#{@app_name}
|
|
140
|
+
"#{@app_name}-${var.environment}-#{Belt::Inflector.pluralize(model_name)}"
|
|
108
141
|
end
|
|
109
142
|
|
|
110
143
|
# Minimal DSL parser for schema.tf.rb
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'fileutils'
|
|
4
4
|
require 'erb'
|
|
5
|
+
require_relative '../inflector'
|
|
5
6
|
|
|
6
7
|
module Belt
|
|
7
8
|
module CLI
|
|
@@ -35,7 +36,7 @@ module Belt
|
|
|
35
36
|
return [] unless File.exist?(schema_file)
|
|
36
37
|
|
|
37
38
|
content = File.read(schema_file)
|
|
38
|
-
singular =
|
|
39
|
+
singular = Belt::Inflector.singularize(name)
|
|
39
40
|
|
|
40
41
|
# Extract fields from model block
|
|
41
42
|
if content =~ /model :#{singular} do\n(.*?)\n\s*end/m
|
|
@@ -53,9 +54,9 @@ module Belt
|
|
|
53
54
|
def initialize(name, fields)
|
|
54
55
|
@name = name.downcase.gsub(/[^a-z0-9_]/, '_')
|
|
55
56
|
@fields = fields
|
|
56
|
-
@
|
|
57
|
-
@
|
|
58
|
-
@class_name = @singular_name
|
|
57
|
+
@singular_name = Belt::Inflector.singularize(@name)
|
|
58
|
+
@resource_name = Belt::Inflector.pluralize(@singular_name)
|
|
59
|
+
@class_name = Belt::Inflector.classify(@singular_name)
|
|
59
60
|
end
|
|
60
61
|
|
|
61
62
|
def generate
|
|
@@ -65,9 +66,10 @@ module Belt
|
|
|
65
66
|
end
|
|
66
67
|
|
|
67
68
|
pages_dir = "frontend/src/pages/#{@resource_name}"
|
|
69
|
+
@plural_class_name = Belt::Inflector.camelize(@resource_name)
|
|
68
70
|
FileUtils.mkdir_p(pages_dir)
|
|
69
71
|
|
|
70
|
-
write_template('Index.jsx.erb', "#{pages_dir}/#{@
|
|
72
|
+
write_template('Index.jsx.erb', "#{pages_dir}/#{@plural_class_name}Index.jsx")
|
|
71
73
|
write_template('Show.jsx.erb', "#{pages_dir}/#{@class_name}Show.jsx")
|
|
72
74
|
write_template('New.jsx.erb', "#{pages_dir}/#{@class_name}New.jsx")
|
|
73
75
|
write_template('Edit.jsx.erb', "#{pages_dir}/#{@class_name}Edit.jsx")
|
|
@@ -77,7 +79,7 @@ module Belt
|
|
|
77
79
|
|
|
78
80
|
puts "\n✓ Views for '#{@singular_name}' generated!"
|
|
79
81
|
puts "\nFiles created:"
|
|
80
|
-
puts " #{pages_dir}/#{@
|
|
82
|
+
puts " #{pages_dir}/#{@plural_class_name}Index.jsx"
|
|
81
83
|
puts " #{pages_dir}/#{@class_name}Show.jsx"
|
|
82
84
|
puts " #{pages_dir}/#{@class_name}New.jsx"
|
|
83
85
|
puts " #{pages_dir}/#{@class_name}Edit.jsx"
|
|
@@ -100,16 +102,17 @@ module Belt
|
|
|
100
102
|
|
|
101
103
|
content = File.read(app_jsx)
|
|
102
104
|
pages_dir = @resource_name
|
|
105
|
+
plural_class = @plural_class_name || Belt::Inflector.camelize(@resource_name)
|
|
103
106
|
|
|
104
107
|
import_lines = [
|
|
105
|
-
"import #{
|
|
108
|
+
"import #{plural_class}Index from './pages/#{pages_dir}/#{plural_class}Index'",
|
|
106
109
|
"import #{@class_name}Show from './pages/#{pages_dir}/#{@class_name}Show'",
|
|
107
110
|
"import #{@class_name}New from './pages/#{pages_dir}/#{@class_name}New'",
|
|
108
111
|
"import #{@class_name}Edit from './pages/#{pages_dir}/#{@class_name}Edit'"
|
|
109
112
|
]
|
|
110
113
|
|
|
111
114
|
route_lines = [
|
|
112
|
-
" <Route path=\"/#{@resource_name}\" element={<#{
|
|
115
|
+
" <Route path=\"/#{@resource_name}\" element={<#{plural_class}Index />} />",
|
|
113
116
|
" <Route path=\"/#{@resource_name}/new\" element={<#{@class_name}New />} />",
|
|
114
117
|
" <Route path=\"/#{@resource_name}/:id\" element={<#{@class_name}Show />} />",
|
|
115
118
|
" <Route path=\"/#{@resource_name}/:id/edit\" element={<#{@class_name}Edit />} />"
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_support/core_ext/string/inflections'
|
|
4
|
+
require 'active_support/inflector'
|
|
5
|
+
|
|
6
|
+
# Add common inflection rules that ActiveSupport doesn't include by default
|
|
7
|
+
ActiveSupport::Inflector.inflections(:en) do |inflect|
|
|
8
|
+
inflect.irregular 'hero', 'heroes'
|
|
9
|
+
inflect.irregular 'potato', 'potatoes'
|
|
10
|
+
inflect.irregular 'volcano', 'volcanoes'
|
|
11
|
+
inflect.irregular 'echo', 'echoes'
|
|
12
|
+
inflect.irregular 'embargo', 'embargoes'
|
|
13
|
+
inflect.irregular 'veto', 'vetoes'
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
module Belt
|
|
17
|
+
# Provides proper English inflection rules (pluralize, singularize, classify, etc.)
|
|
18
|
+
# via ActiveSupport::Inflector. Used throughout Belt for resource name derivation.
|
|
19
|
+
#
|
|
20
|
+
# ActiveSupport is already a transitive dependency (via activeitem → activemodel → activesupport)
|
|
21
|
+
# so this adds zero new weight.
|
|
22
|
+
module Inflector
|
|
23
|
+
module_function
|
|
24
|
+
|
|
25
|
+
# "hero" → "heroes", "post" → "posts", "person" → "people"
|
|
26
|
+
def pluralize(word)
|
|
27
|
+
word.to_s.pluralize
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# "heroes" → "hero", "posts" → "post", "people" → "person"
|
|
31
|
+
def singularize(word)
|
|
32
|
+
word.to_s.singularize
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# "blog_post" → "BlogPost", "hero" → "Hero"
|
|
36
|
+
def classify(word)
|
|
37
|
+
singularize(word).split('_').map(&:capitalize).join
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# "blog_posts" → "BlogPosts", "heroes" → "Heroes" (no singularization)
|
|
41
|
+
def camelize(word)
|
|
42
|
+
word.to_s.split('_').map(&:capitalize).join
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# "BlogPost" → "blog_post"
|
|
46
|
+
def underscore(word)
|
|
47
|
+
word.to_s
|
|
48
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
49
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
50
|
+
.downcase
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# "heroes_controller" → "heroes", "PostsController" → "posts"
|
|
54
|
+
def resource_name(word)
|
|
55
|
+
pluralize(word.to_s.sub(/_?controller$/i, '').downcase)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
data/lib/belt/lambda_handler.rb
CHANGED
|
@@ -41,6 +41,17 @@ module Belt
|
|
|
41
41
|
subdir = File.join(controllers_dir, child)
|
|
42
42
|
Belt.controller_paths << subdir if File.directory?(subdir)
|
|
43
43
|
end
|
|
44
|
+
|
|
45
|
+
# Auto-load all models (application_record first, then the rest)
|
|
46
|
+
models_dir = File.join(File.dirname(caller_file), 'models')
|
|
47
|
+
if File.directory?(models_dir)
|
|
48
|
+
all_models = Dir[File.join(models_dir, '**', '*.rb')].sort
|
|
49
|
+
app_record = all_models.find { |f| File.basename(f) == 'application_record.rb' }
|
|
50
|
+
rest = all_models - [app_record].compact
|
|
51
|
+
|
|
52
|
+
require app_record if app_record
|
|
53
|
+
rest.each { |f| require f }
|
|
54
|
+
end
|
|
44
55
|
end
|
|
45
56
|
|
|
46
57
|
# API Gateway Lambda handler.
|
data/lib/belt/route_dsl.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative 'inflector'
|
|
4
|
+
|
|
3
5
|
module Belt
|
|
4
6
|
# DSL for defining API Gateway routes.
|
|
5
7
|
# Ported from terraform-provider-conveyor-belt/scripts/lib/route_dsl.rb
|
|
@@ -256,17 +258,7 @@ module Belt
|
|
|
256
258
|
end
|
|
257
259
|
|
|
258
260
|
def singularize(word)
|
|
259
|
-
|
|
260
|
-
"#{word[0..-4]}y"
|
|
261
|
-
elsif word.end_with?('xes') || word.end_with?('zes') || word.end_with?('ses')
|
|
262
|
-
word[0..-3]
|
|
263
|
-
elsif word.end_with?('ches') || word.end_with?('shes')
|
|
264
|
-
word[0..-3]
|
|
265
|
-
elsif word.end_with?('s') && !word.end_with?('ss')
|
|
266
|
-
word[0..-2]
|
|
267
|
-
else
|
|
268
|
-
word
|
|
269
|
-
end
|
|
261
|
+
Belt::Inflector.singularize(word)
|
|
270
262
|
end
|
|
271
263
|
end
|
|
272
264
|
|
data/lib/belt/table_inference.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative 'inflector'
|
|
4
|
+
|
|
3
5
|
module Belt
|
|
4
6
|
# Infers DynamoDB table names from route paths by matching against
|
|
5
7
|
# tables defined in a Terraform file containing aws_dynamodb_table resources.
|
|
@@ -45,27 +47,11 @@ module Belt
|
|
|
45
47
|
end
|
|
46
48
|
|
|
47
49
|
def pluralize(word)
|
|
48
|
-
|
|
49
|
-
"#{word[0..-2]}ies"
|
|
50
|
-
elsif word.end_with?('s', 'x', 'z', 'ch', 'sh')
|
|
51
|
-
"#{word}es"
|
|
52
|
-
else
|
|
53
|
-
"#{word}s"
|
|
54
|
-
end
|
|
50
|
+
Belt::Inflector.pluralize(word)
|
|
55
51
|
end
|
|
56
52
|
|
|
57
53
|
def singularize(word)
|
|
58
|
-
|
|
59
|
-
"#{word[0..-4]}y"
|
|
60
|
-
elsif word.end_with?('xes', 'zes', 'ses')
|
|
61
|
-
word[0..-3]
|
|
62
|
-
elsif word.end_with?('ches', 'shes')
|
|
63
|
-
word[0..-3]
|
|
64
|
-
elsif word.end_with?('s') && !word.end_with?('ss')
|
|
65
|
-
word[0..-2]
|
|
66
|
-
else
|
|
67
|
-
word
|
|
68
|
-
end
|
|
54
|
+
Belt::Inflector.singularize(word)
|
|
69
55
|
end
|
|
70
56
|
end
|
|
71
57
|
end
|
data/lib/belt/version.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
terraform {
|
|
2
2
|
backend "s3" {
|
|
3
|
-
bucket = "
|
|
4
|
-
key = "<%= @env_name %>/terraform.tfstate"
|
|
3
|
+
bucket = "belt-terraform-state"
|
|
4
|
+
key = "<%= s3_safe_name(@app_name) %>/<%= @env_name %>/terraform.tfstate"
|
|
5
5
|
region = "us-east-1"
|
|
6
6
|
encrypt = true
|
|
7
7
|
}
|