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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c3c75f106f7a0f2ebf672294ee30e96f96745fb3ecbd142307c08b6b006933c8
|
|
4
|
+
data.tar.gz: 470cefb769cfab7dbaf80e6f950eb62cf87fb5a6b297f3661cf147c26d494501
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e5c5ed3dfa729483fd54e28197c9bd2f47f3581bea5f0892a4ac44c02f55253809f4fe1c12167ca55a040bdebc97e545b462f08d2f42c23dd1304a4a9dfd7768
|
|
7
|
+
data.tar.gz: be36355b315e467f411079880a4b3e14e94b56567cbf1892a95fff3f2b3c1c9a8bfe91e2e8618b405b1b06045ee1c06e999c12a6c281b9c490b8e8dc43d8f999
|
|
@@ -503,8 +503,48 @@ module Belt
|
|
|
503
503
|
output.each_line do |line|
|
|
504
504
|
puts " #{line}"
|
|
505
505
|
end
|
|
506
|
+
|
|
507
|
+
# Show DNS guidance if name_servers are present and this looks like a first deploy
|
|
508
|
+
print_dns_guidance if output.include?('name_servers')
|
|
506
509
|
end
|
|
507
510
|
end
|
|
511
|
+
|
|
512
|
+
def print_dns_guidance
|
|
513
|
+
# Check if there's a marker file indicating DNS guidance was already shown
|
|
514
|
+
marker = File.join(@infra_dir, @env, '.dns_configured')
|
|
515
|
+
return if File.exist?(marker)
|
|
516
|
+
|
|
517
|
+
domain = detect_domain_from_tfvars
|
|
518
|
+
return unless domain
|
|
519
|
+
|
|
520
|
+
puts ''
|
|
521
|
+
puts ' ┌─────────────────────────────────────────────────────────────┐'
|
|
522
|
+
puts ' │ DNS SETUP REQUIRED │'
|
|
523
|
+
puts ' ├─────────────────────────────────────────────────────────────┤'
|
|
524
|
+
puts ' │ Point your domain to the name_servers shown above. │'
|
|
525
|
+
puts ' │ │'
|
|
526
|
+
puts ' │ • External registrar (GoDaddy, Namecheap, etc.): │'
|
|
527
|
+
puts ' │ Update nameservers in your registrar\'s DNS settings. │'
|
|
528
|
+
puts ' │ │'
|
|
529
|
+
puts ' │ • Domain in AWS Route53 Registered Domains: │'
|
|
530
|
+
puts ' │ Route53 → Registered Domains → Name servers → Edit. │'
|
|
531
|
+
puts ' │ │'
|
|
532
|
+
puts " │ Verify: dig +short NS #{domain.ljust(38)}│"
|
|
533
|
+
puts ' │ │'
|
|
534
|
+
puts ' │ This message won\'t appear on subsequent deploys. │'
|
|
535
|
+
puts ' └─────────────────────────────────────────────────────────────┘'
|
|
536
|
+
|
|
537
|
+
# Create the marker so we don't nag on every deploy
|
|
538
|
+
File.write(marker, "DNS guidance shown at #{Time.now}\n")
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
def detect_domain_from_tfvars
|
|
542
|
+
tfvars_file = File.join(@infra_dir, @env, 'terraform.tfvars')
|
|
543
|
+
return nil unless File.exist?(tfvars_file)
|
|
544
|
+
|
|
545
|
+
match = File.read(tfvars_file).match(/^\s*domain\s*=\s*"([^"]+)"/)
|
|
546
|
+
match[1] if match
|
|
547
|
+
end
|
|
508
548
|
end
|
|
509
549
|
end
|
|
510
550
|
end
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require 'fileutils'
|
|
4
4
|
require_relative 'app_detection'
|
|
5
|
+
require_relative 'tables_command'
|
|
6
|
+
require_relative '../inflector'
|
|
5
7
|
|
|
6
8
|
module Belt
|
|
7
9
|
module CLI
|
|
@@ -93,13 +95,9 @@ module Belt
|
|
|
93
95
|
@name = name&.downcase&.gsub(/[^a-z0-9_]/, '_')
|
|
94
96
|
@fields = fields
|
|
95
97
|
@app_name = detect_namespace
|
|
96
|
-
@
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
@singular_name = if @name
|
|
100
|
-
@name.end_with?('s') ? @name.chomp('s') : @name
|
|
101
|
-
end
|
|
102
|
-
@class_name = @singular_name&.split('_')&.map(&:capitalize)&.join
|
|
98
|
+
@singular_name = @name ? Belt::Inflector.singularize(@name) : nil
|
|
99
|
+
@resource_name = @singular_name ? Belt::Inflector.pluralize(@singular_name) : nil
|
|
100
|
+
@class_name = @singular_name ? Belt::Inflector.classify(@singular_name) : nil
|
|
103
101
|
@removed = []
|
|
104
102
|
@updated = []
|
|
105
103
|
end
|
|
@@ -123,7 +121,6 @@ module Belt
|
|
|
123
121
|
destroy_model
|
|
124
122
|
destroy_controller
|
|
125
123
|
remove_routes
|
|
126
|
-
remove_schema
|
|
127
124
|
destroy_views
|
|
128
125
|
puts "\n✓ Scaffold '#{@singular_name}' destroyed!"
|
|
129
126
|
end
|
|
@@ -131,6 +128,8 @@ module Belt
|
|
|
131
128
|
def destroy_model
|
|
132
129
|
path = "lambda/models/#{@singular_name}.rb"
|
|
133
130
|
remove_file(path)
|
|
131
|
+
remove_schema
|
|
132
|
+
sync_tables
|
|
134
133
|
end
|
|
135
134
|
|
|
136
135
|
def destroy_controller
|
|
@@ -157,6 +156,15 @@ module Belt
|
|
|
157
156
|
FileUtils.rm_rf(dir)
|
|
158
157
|
@removed << dir
|
|
159
158
|
puts " remove #{dir}/"
|
|
159
|
+
|
|
160
|
+
# Remove frontend.tf from the module
|
|
161
|
+
module_frontend = 'infrastructure/modules/app/frontend.tf'
|
|
162
|
+
if File.exist?(module_frontend)
|
|
163
|
+
FileUtils.rm_f(module_frontend)
|
|
164
|
+
@removed << module_frontend
|
|
165
|
+
puts " remove #{module_frontend}"
|
|
166
|
+
end
|
|
167
|
+
|
|
160
168
|
puts "\n✓ Frontend removed!"
|
|
161
169
|
else
|
|
162
170
|
puts '✗ No frontend/ directory found.'
|
|
@@ -168,6 +176,7 @@ module Belt
|
|
|
168
176
|
return unless @resource_name
|
|
169
177
|
|
|
170
178
|
pages_dir = "frontend/src/pages/#{@resource_name}"
|
|
179
|
+
|
|
171
180
|
if Dir.exist?(pages_dir)
|
|
172
181
|
FileUtils.rm_rf(pages_dir)
|
|
173
182
|
@removed << pages_dir
|
|
@@ -255,6 +264,10 @@ module Belt
|
|
|
255
264
|
end
|
|
256
265
|
end
|
|
257
266
|
|
|
267
|
+
def sync_tables
|
|
268
|
+
Belt::CLI::TablesCommand.sync_all_environments
|
|
269
|
+
end
|
|
270
|
+
|
|
258
271
|
def remove_view_routes
|
|
259
272
|
app_jsx = 'frontend/src/App.jsx'
|
|
260
273
|
return unless File.exist?(app_jsx)
|
|
@@ -26,9 +26,10 @@ module Belt
|
|
|
26
26
|
new(env_name).generate
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
-
def initialize(env_name, quiet: false)
|
|
29
|
+
def initialize(env_name, quiet: false, domain: nil)
|
|
30
30
|
@env_name = env_name.downcase.gsub(/[^a-z0-9_-]/, '')
|
|
31
31
|
@app_name = detect_app_name
|
|
32
|
+
@domain = domain
|
|
32
33
|
@quiet = quiet
|
|
33
34
|
end
|
|
34
35
|
|
|
@@ -51,28 +52,19 @@ module Belt
|
|
|
51
52
|
|
|
52
53
|
puts "\n✓ Environment '#{@env_name}' created!"
|
|
53
54
|
|
|
54
|
-
setup_frontend_if_exists
|
|
55
|
-
|
|
56
55
|
unless @quiet
|
|
57
56
|
puts "\nNext steps:"
|
|
58
57
|
puts " cd #{dest_dir}"
|
|
59
58
|
puts ' terraform init'
|
|
60
59
|
puts ' terraform plan'
|
|
61
60
|
puts ' terraform apply'
|
|
61
|
+
puts "\nTo configure a custom domain, set `domain` in #{dest_dir}/terraform.tfvars:"
|
|
62
|
+
puts " domain = \"myapp.com\""
|
|
62
63
|
end
|
|
63
64
|
end
|
|
64
65
|
|
|
65
66
|
private
|
|
66
67
|
|
|
67
|
-
def setup_frontend_if_exists
|
|
68
|
-
return unless Dir.exist?('frontend') && !Dir.empty?('frontend')
|
|
69
|
-
|
|
70
|
-
puts "\n Frontend detected — generating frontend infrastructure..."
|
|
71
|
-
require_relative 'frontend_setup_command'
|
|
72
|
-
FrontendSetupCommand.new(@env_name, quiet: true).run
|
|
73
|
-
puts " create infrastructure/#{@env_name}/frontend.tf"
|
|
74
|
-
end
|
|
75
|
-
|
|
76
68
|
def templates
|
|
77
69
|
{
|
|
78
70
|
'main.tf.erb' => 'main.tf',
|
|
@@ -75,22 +75,20 @@ module Belt
|
|
|
75
75
|
end
|
|
76
76
|
|
|
77
77
|
def setup_frontend_infra_for_existing_environments
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
module_dir = 'infrastructure/modules/app'
|
|
79
|
+
frontend_tf = File.join(module_dir, 'frontend.tf')
|
|
80
80
|
|
|
81
|
-
|
|
82
|
-
|
|
81
|
+
if File.exist?(frontend_tf)
|
|
82
|
+
puts " skip #{frontend_tf} (already exists)"
|
|
83
|
+
return
|
|
84
|
+
end
|
|
83
85
|
|
|
84
|
-
|
|
85
|
-
frontend_tf = "infrastructure/#{env_name}/frontend.tf"
|
|
86
|
-
if File.exist?(frontend_tf)
|
|
87
|
-
puts " skip #{frontend_tf} (already exists)"
|
|
88
|
-
next
|
|
89
|
-
end
|
|
86
|
+
return unless Dir.exist?(module_dir)
|
|
90
87
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
88
|
+
puts "\n Setting up frontend infrastructure..."
|
|
89
|
+
require_relative 'frontend_setup_command'
|
|
90
|
+
FrontendSetupCommand.new(nil, quiet: true).run
|
|
91
|
+
puts " create #{frontend_tf}"
|
|
94
92
|
end
|
|
95
93
|
|
|
96
94
|
def copy_template(src_dir, dest_dir)
|
|
@@ -8,31 +8,20 @@ require_relative 'env_resolver'
|
|
|
8
8
|
module Belt
|
|
9
9
|
module CLI
|
|
10
10
|
class FrontendSetupCommand
|
|
11
|
-
TEMPLATE_DIR = File.expand_path('../../templates/
|
|
11
|
+
TEMPLATE_DIR = File.expand_path('../../templates/module', __dir__)
|
|
12
|
+
MODULE_DIR = 'infrastructure/modules/app'
|
|
12
13
|
|
|
13
14
|
include AppDetection
|
|
14
15
|
|
|
15
16
|
def self.run(args)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
puts "\nGenerates S3 + CloudFront Terraform for frontend hosting."
|
|
21
|
-
puts 'You can also set BELT_ENV to skip the environment argument.'
|
|
22
|
-
puts "\nExamples:"
|
|
23
|
-
puts ' belt setup frontend wups'
|
|
24
|
-
puts ' belt setup frontend dev01'
|
|
25
|
-
puts ' BELT_ENV=wups belt setup frontend'
|
|
26
|
-
exit 1
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
new(env).run
|
|
17
|
+
# Environment arg is no longer needed since frontend.tf goes in the module,
|
|
18
|
+
# but we still accept it for backwards compat (just ignore it).
|
|
19
|
+
_env = EnvResolver.resolve(args)
|
|
20
|
+
new.run
|
|
30
21
|
end
|
|
31
22
|
|
|
32
|
-
def initialize(env, quiet: false)
|
|
33
|
-
@env = env
|
|
23
|
+
def initialize(env = nil, quiet: false)
|
|
34
24
|
@app_name = detect_app_name
|
|
35
|
-
@env_dir = "infrastructure/#{@env}"
|
|
36
25
|
@quiet = quiet
|
|
37
26
|
end
|
|
38
27
|
|
|
@@ -41,22 +30,22 @@ module Belt
|
|
|
41
30
|
generate_frontend_tf
|
|
42
31
|
return if @quiet
|
|
43
32
|
|
|
44
|
-
puts "\n✓ Frontend infrastructure generated
|
|
45
|
-
puts "\nRun `belt
|
|
46
|
-
puts "Then `belt deploy frontend
|
|
33
|
+
puts "\n✓ Frontend infrastructure generated in #{MODULE_DIR}!"
|
|
34
|
+
puts "\nRun `belt deploy` to create the S3 bucket and CloudFront distribution."
|
|
35
|
+
puts "Then `belt deploy frontend` to build and deploy."
|
|
47
36
|
end
|
|
48
37
|
|
|
49
38
|
private
|
|
50
39
|
|
|
51
40
|
def validate!
|
|
52
|
-
return if Dir.exist?(
|
|
41
|
+
return if Dir.exist?(MODULE_DIR)
|
|
53
42
|
|
|
54
|
-
abort "Error:
|
|
55
|
-
"
|
|
43
|
+
abort "Error: Module directory not found at #{MODULE_DIR}/.\n" \
|
|
44
|
+
"Run `belt new` to create a project with the correct structure."
|
|
56
45
|
end
|
|
57
46
|
|
|
58
47
|
def generate_frontend_tf
|
|
59
|
-
dest = File.join(
|
|
48
|
+
dest = File.join(MODULE_DIR, 'frontend.tf')
|
|
60
49
|
template_path = File.join(TEMPLATE_DIR, 'frontend.tf.erb')
|
|
61
50
|
content = ERB.new(File.read(template_path), trim_mode: '-').result(binding)
|
|
62
51
|
File.write(dest, content)
|
|
@@ -5,7 +5,9 @@ require 'erb'
|
|
|
5
5
|
require_relative 'app_detection'
|
|
6
6
|
require_relative 'environment_command'
|
|
7
7
|
require_relative 'frontend_command'
|
|
8
|
+
require_relative 'tables_command'
|
|
8
9
|
require_relative 'views_command'
|
|
10
|
+
require_relative '../inflector'
|
|
9
11
|
|
|
10
12
|
module Belt
|
|
11
13
|
module CLI
|
|
@@ -22,12 +24,14 @@ module Belt
|
|
|
22
24
|
description: 'Generate a model, controller, routes, schema, and views for a REST resource.',
|
|
23
25
|
usage: 'belt generate scaffold <name> [field:type ...] [options]',
|
|
24
26
|
options: [
|
|
25
|
-
['--skip-views', 'Skip generating frontend view pages']
|
|
27
|
+
['--skip-views', 'Skip generating frontend view pages'],
|
|
28
|
+
['--force, -f', 'Overwrite existing resource files (skip collision check)']
|
|
26
29
|
],
|
|
27
30
|
examples: [
|
|
28
|
-
['belt g scaffold post title
|
|
29
|
-
['belt g scaffold comment author
|
|
30
|
-
['belt g scaffold task --skip-views',
|
|
31
|
+
['belt g scaffold post title body:text'],
|
|
32
|
+
['belt g scaffold comment author body:text status'],
|
|
33
|
+
['belt g scaffold task --skip-views'],
|
|
34
|
+
['belt g scaffold post title body:text --force']
|
|
31
35
|
],
|
|
32
36
|
notes: <<~NOTES
|
|
33
37
|
Creates:
|
|
@@ -43,11 +47,13 @@ module Belt
|
|
|
43
47
|
},
|
|
44
48
|
'model' => {
|
|
45
49
|
description: 'Generate an ActiveItem model with validations and DynamoDB field definitions.',
|
|
46
|
-
usage: 'belt generate model <name> [field:type ...]',
|
|
47
|
-
options: [
|
|
50
|
+
usage: 'belt generate model <name> [field:type ...] [options]',
|
|
51
|
+
options: [
|
|
52
|
+
['--force, -f', 'Overwrite existing model files (skip collision check)']
|
|
53
|
+
],
|
|
48
54
|
examples: [
|
|
49
|
-
['belt g model user email
|
|
50
|
-
['belt g model event title
|
|
55
|
+
['belt g model user email name'],
|
|
56
|
+
['belt g model event title starts_at:datetime']
|
|
51
57
|
],
|
|
52
58
|
notes: <<~NOTES
|
|
53
59
|
Creates:
|
|
@@ -59,8 +65,8 @@ module Belt
|
|
|
59
65
|
usage: 'belt generate controller <name>',
|
|
60
66
|
options: [],
|
|
61
67
|
examples: [
|
|
62
|
-
['belt g controller posts'
|
|
63
|
-
['belt g controller admin/users'
|
|
68
|
+
['belt g controller posts'],
|
|
69
|
+
['belt g controller admin/users']
|
|
64
70
|
],
|
|
65
71
|
notes: <<~NOTES
|
|
66
72
|
Creates:
|
|
@@ -108,9 +114,10 @@ module Belt
|
|
|
108
114
|
|
|
109
115
|
validate_resource_name!(name, generator)
|
|
110
116
|
|
|
117
|
+
force = args.delete('--force') || args.delete('-f')
|
|
111
118
|
skip_views = args.delete('--skip-views')
|
|
112
119
|
fields = args.map { |arg| parse_field(arg) }
|
|
113
|
-
new(generator, name, fields, skip_views: skip_views).generate
|
|
120
|
+
new(generator, name, fields, skip_views: skip_views, force: force).generate
|
|
114
121
|
end
|
|
115
122
|
|
|
116
123
|
def self.parse_field(arg)
|
|
@@ -132,7 +139,7 @@ module Belt
|
|
|
132
139
|
errors << 'must only contain letters, numbers, and underscores' unless name.match?(/\A[a-zA-Z][a-zA-Z0-9_]*\z/)
|
|
133
140
|
errors << 'must not start or end with an underscore' if name.match?(/\A_|_\z/)
|
|
134
141
|
errors << 'must not contain consecutive underscores' if name.include?('__')
|
|
135
|
-
errors << "is a reserved name" if RESERVED_NAMES.include?(name.downcase
|
|
142
|
+
errors << "is a reserved name" if RESERVED_NAMES.include?(Belt::Inflector.singularize(name.downcase))
|
|
136
143
|
errors << "is a reserved name" if RESERVED_NAMES.include?(name.downcase)
|
|
137
144
|
|
|
138
145
|
return if errors.empty?
|
|
@@ -161,14 +168,15 @@ module Belt
|
|
|
161
168
|
|
|
162
169
|
Field Types:
|
|
163
170
|
#{FIELD_TYPES.join(', ')}
|
|
171
|
+
(defaults to string if omitted)
|
|
164
172
|
|
|
165
173
|
Examples:
|
|
166
|
-
belt g scaffold post title
|
|
167
|
-
belt g model user email
|
|
174
|
+
belt g scaffold post title body:text status
|
|
175
|
+
belt g model user email name
|
|
168
176
|
belt g controller comments
|
|
169
177
|
belt g environment staging
|
|
170
178
|
belt g frontend react
|
|
171
|
-
belt g views post title
|
|
179
|
+
belt g views post title body:text
|
|
172
180
|
|
|
173
181
|
Run 'belt generate <generator> --help' for detailed help on a specific generator.
|
|
174
182
|
HELP
|
|
@@ -197,50 +205,95 @@ module Belt
|
|
|
197
205
|
|
|
198
206
|
puts "\nField Types:"
|
|
199
207
|
puts " #{FIELD_TYPES.join(', ')}"
|
|
208
|
+
puts " (defaults to string if omitted)"
|
|
200
209
|
|
|
201
210
|
puts "\nExamples:"
|
|
202
211
|
info[:examples].each do |cmd, desc|
|
|
203
|
-
|
|
204
|
-
|
|
212
|
+
if desc
|
|
213
|
+
puts " #{cmd} — #{desc}"
|
|
214
|
+
else
|
|
215
|
+
puts " #{cmd}"
|
|
216
|
+
end
|
|
205
217
|
end
|
|
206
218
|
|
|
207
219
|
puts "\n#{info[:notes]}" if info[:notes]
|
|
208
|
-
|
|
209
|
-
puts "Naming Rules:"
|
|
210
|
-
puts " - Must start with a letter"
|
|
211
|
-
puts " - At least 2 characters"
|
|
212
|
-
puts " - Letters, numbers, and single underscores only"
|
|
213
|
-
puts " - No leading/trailing/consecutive underscores"
|
|
214
|
-
puts " - Cannot use reserved names (help, new, create, destroy, etc.)"
|
|
215
220
|
end
|
|
216
221
|
|
|
217
|
-
def initialize(generator, name, fields, skip_views: false)
|
|
222
|
+
def initialize(generator, name, fields, skip_views: false, force: false)
|
|
218
223
|
@generator = generator
|
|
219
224
|
@name = name.downcase.gsub(/[^a-z0-9_]/, '_')
|
|
220
225
|
@fields = fields
|
|
221
226
|
@skip_views = skip_views
|
|
227
|
+
@force = force
|
|
222
228
|
@app_name = detect_namespace
|
|
223
229
|
@module_name = @app_name.split(/[-_]/).map(&:capitalize).join
|
|
224
|
-
@
|
|
225
|
-
@
|
|
226
|
-
@class_name = @singular_name
|
|
230
|
+
@singular_name = Belt::Inflector.singularize(@name)
|
|
231
|
+
@resource_name = Belt::Inflector.pluralize(@singular_name)
|
|
232
|
+
@class_name = Belt::Inflector.classify(@singular_name)
|
|
227
233
|
end
|
|
228
234
|
|
|
229
235
|
def generate
|
|
230
236
|
case @generator
|
|
231
|
-
when 'scaffold'
|
|
232
|
-
|
|
237
|
+
when 'scaffold'
|
|
238
|
+
check_resource_collision! unless @force
|
|
239
|
+
generate_resource
|
|
240
|
+
when 'model'
|
|
241
|
+
check_model_collision! unless @force
|
|
242
|
+
generate_model_standalone
|
|
233
243
|
when 'controller' then generate_controller
|
|
234
244
|
end
|
|
235
245
|
end
|
|
236
246
|
|
|
237
247
|
private
|
|
238
248
|
|
|
249
|
+
def check_resource_collision!
|
|
250
|
+
conflicts = []
|
|
251
|
+
conflicts << "lambda/models/#{@singular_name}.rb" if File.exist?("lambda/models/#{@singular_name}.rb")
|
|
252
|
+
conflicts << "lambda/controllers/#{@app_name}/#{@resource_name}_controller.rb" if File.exist?("lambda/controllers/#{@app_name}/#{@resource_name}_controller.rb")
|
|
253
|
+
|
|
254
|
+
schema_file = 'infrastructure/schema.tf.rb'
|
|
255
|
+
if File.exist?(schema_file) && File.read(schema_file).match?(/^\s*model :#{Regexp.escape(@singular_name)}\b/)
|
|
256
|
+
conflicts << "#{schema_file} (model :#{@singular_name})"
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
routes_file = 'infrastructure/routes.tf.rb'
|
|
260
|
+
if File.exist?(routes_file) && File.read(routes_file).match?(/resources :#{Regexp.escape(@resource_name)}\b/)
|
|
261
|
+
conflicts << "#{routes_file} (resources :#{@resource_name})"
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
return if conflicts.empty?
|
|
265
|
+
|
|
266
|
+
puts "\n✗ Resource '#{@singular_name}' already exists. Conflicting files:"
|
|
267
|
+
conflicts.each { |c| puts " • #{c}" }
|
|
268
|
+
puts "\nTo overwrite, run again with --force:"
|
|
269
|
+
puts " belt g scaffold #{@singular_name} #{@fields.map { |f| "#{f[:name]}:#{f[:type]}" }.join(' ')} --force"
|
|
270
|
+
exit 1
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def check_model_collision!
|
|
274
|
+
conflicts = []
|
|
275
|
+
conflicts << "lambda/models/#{@singular_name}.rb" if File.exist?("lambda/models/#{@singular_name}.rb")
|
|
276
|
+
|
|
277
|
+
schema_file = 'infrastructure/schema.tf.rb'
|
|
278
|
+
if File.exist?(schema_file) && File.read(schema_file).match?(/^\s*model :#{Regexp.escape(@singular_name)}\b/)
|
|
279
|
+
conflicts << "#{schema_file} (model :#{@singular_name})"
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
return if conflicts.empty?
|
|
283
|
+
|
|
284
|
+
puts "\n✗ Model '#{@singular_name}' already exists. Conflicting files:"
|
|
285
|
+
conflicts.each { |c| puts " • #{c}" }
|
|
286
|
+
puts "\nTo overwrite, run again with --force:"
|
|
287
|
+
puts " belt g model #{@singular_name} #{@fields.map { |f| "#{f[:name]}:#{f[:type]}" }.join(' ')} --force"
|
|
288
|
+
exit 1
|
|
289
|
+
end
|
|
290
|
+
|
|
239
291
|
def generate_resource
|
|
240
292
|
generate_model
|
|
241
293
|
generate_controller
|
|
242
294
|
inject_routes
|
|
243
295
|
inject_schema
|
|
296
|
+
sync_tables
|
|
244
297
|
generate_views_if_frontend
|
|
245
298
|
puts "\n✓ Scaffold '#{@singular_name}' generated!"
|
|
246
299
|
puts "\nFiles created/updated:"
|
|
@@ -252,6 +305,12 @@ module Belt
|
|
|
252
305
|
puts " frontend/src/pages/#{@resource_name}/ (views)" if Dir.exist?('frontend/src')
|
|
253
306
|
end
|
|
254
307
|
|
|
308
|
+
def generate_model_standalone
|
|
309
|
+
generate_model
|
|
310
|
+
inject_schema
|
|
311
|
+
sync_tables
|
|
312
|
+
end
|
|
313
|
+
|
|
255
314
|
def generate_model
|
|
256
315
|
dest = "lambda/models/#{@singular_name}.rb"
|
|
257
316
|
write_template('model.rb.erb', dest)
|
|
@@ -272,8 +331,15 @@ module Belt
|
|
|
272
331
|
tables_arg = @fields.any? ? ", tables: [:#{@resource_name}]" : ''
|
|
273
332
|
resource_line = "resources :#{@resource_name}#{tables_arg}"
|
|
274
333
|
|
|
334
|
+
# If this resource already exists in routes (force mode), replace it
|
|
335
|
+
existing_resource_pattern = /^\s*resources :#{Regexp.escape(@resource_name)}\b[^\n]*/
|
|
336
|
+
if content.match?(existing_resource_pattern)
|
|
337
|
+
content.sub!(existing_resource_pattern) do |match|
|
|
338
|
+
indent = match[/^\s*/]
|
|
339
|
+
"#{indent}#{resource_line}"
|
|
340
|
+
end
|
|
275
341
|
# Replace the commented placeholder if it exists
|
|
276
|
-
|
|
342
|
+
elsif content.include?('# resources :posts')
|
|
277
343
|
content.sub!('# resources :posts', resource_line)
|
|
278
344
|
else
|
|
279
345
|
# Find the target namespace block and insert before its closing `end`
|
|
@@ -361,8 +427,12 @@ module Belt
|
|
|
361
427
|
|
|
362
428
|
schema_block = " model :#{@singular_name} do\n#{field_lines.join("\n")}\n end\n"
|
|
363
429
|
|
|
430
|
+
# If model already exists (force mode), replace it
|
|
431
|
+
existing_model_pattern = /^ model :#{Regexp.escape(@singular_name)} do\n.*?^ end\n/m
|
|
432
|
+
if content.match?(existing_model_pattern)
|
|
433
|
+
content.sub!(existing_model_pattern, schema_block)
|
|
364
434
|
# Replace commented-out block or insert before final end
|
|
365
|
-
|
|
435
|
+
elsif content.match?(/^\s*#\s*model :/)
|
|
366
436
|
content.gsub!(/^\s*#[^\n]*\n/, '')
|
|
367
437
|
content.sub!(/^(end\s*\z)/m, "#{schema_block}\\1")
|
|
368
438
|
else
|
|
@@ -373,6 +443,10 @@ module Belt
|
|
|
373
443
|
puts " update #{schema_file}"
|
|
374
444
|
end
|
|
375
445
|
|
|
446
|
+
def sync_tables
|
|
447
|
+
Belt::CLI::TablesCommand.sync_all_environments
|
|
448
|
+
end
|
|
449
|
+
|
|
376
450
|
def write_template(template_name, dest_path)
|
|
377
451
|
template_path = File.join(TEMPLATE_DIR, template_name)
|
|
378
452
|
FileUtils.mkdir_p(File.dirname(dest_path))
|