belt 0.2.12 → 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 +11 -1
- data/lib/belt/cli/frontend_command.rb +23 -0
- data/lib/belt/cli/generate_command.rb +6 -4
- data/lib/belt/cli/logs_command.rb +634 -0
- data/lib/belt/cli/views_command.rb +89 -9
- data/lib/belt/cli.rb +3 -0
- data/lib/belt/route_dsl.rb +7 -4
- data/lib/belt/version.rb +1 -1
- data/lib/belt.rb +1 -0
- data/lib/templates/generate/controller.rb.erb +13 -18
- data/lib/templates/generate/model.rb.erb +1 -14
- data/lib/templates/new_app/Gemfile.erb +0 -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
- metadata +8 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 43bc6e6cb774005faa3bab6b5133403255636327e3dd017ad4acad8fd9a2c884
|
|
4
|
+
data.tar.gz: fe11f89e1efa821750dea2953c70bbc21fe24bea394d418c2b870a2804adf331
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d5c4900624a6435b178455b5aa7e6f34c08d7c2e75e2d1ec3c1b69e717a5964a390ba59a4ff8ccad1be797d29273bddc4354c235f83070d00bb7517d60bcb38e
|
|
7
|
+
data.tar.gz: a47bc3063743a278e4241faf65c510ede23ec922c167469f2654627f3079bb558893194f717979002354e07153e41cee257785e8b0f7dea187fc391fb351e5f3
|
data/CHANGELOG.md
CHANGED
|
@@ -31,7 +31,17 @@ Previously, contracts were only accessible as a side-effect of `belt routes -f j
|
|
|
31
31
|
- Module template updated: `source` points to `config/routes.rb`
|
|
32
32
|
- All scaffold/generator help text and templates reference new filenames
|
|
33
33
|
|
|
34
|
-
##
|
|
34
|
+
## 0.2.13
|
|
35
|
+
|
|
36
|
+
### Template cleanup
|
|
37
|
+
|
|
38
|
+
- **Model template**: single-line `attr_accessor` instead of one per attribute; removed redundant `to_h` (ActiveItem::Base already provides it)
|
|
39
|
+
- **Controller template**: uses implicit response pattern (instance variable assigns) instead of explicit `success_response` calls; `response_status :created` for create actions, `head :no_content` for destroy
|
|
40
|
+
- **Gemfile template**: removed `activeitem` and `lambda_loadout` (already belt gem dependencies)
|
|
41
|
+
- **gitignore template**: excludes `lambda/lib/routes/` (generated artifact from `belt routes`)
|
|
42
|
+
- **Removed `require 'activeitem'`** from api.rb, environment.rb, and application_record.rb templates (belt requires it transitively)
|
|
43
|
+
- **Removed `ActiveItem.configure` boilerplate** from api.rb and environment.rb templates (activeitem 0.0.13+ defaults `table_prefix` and `environment` from ENV vars)
|
|
44
|
+
- Bumped activeitem dependency to `>= 0.0.13`
|
|
35
45
|
|
|
36
46
|
## 0.2.11
|
|
37
47
|
|
|
@@ -59,6 +59,7 @@ module Belt
|
|
|
59
59
|
|
|
60
60
|
install_dependencies(dest_dir)
|
|
61
61
|
setup_frontend_infra_for_existing_environments
|
|
62
|
+
generate_views_for_existing_resources
|
|
62
63
|
|
|
63
64
|
return if @quiet || !@announce
|
|
64
65
|
|
|
@@ -104,6 +105,28 @@ module Belt
|
|
|
104
105
|
puts " create #{frontend_tf}" unless @quiet
|
|
105
106
|
end
|
|
106
107
|
|
|
108
|
+
def generate_views_for_existing_resources
|
|
109
|
+
routes_file = find_routes_file_path
|
|
110
|
+
return unless routes_file && File.exist?(routes_file)
|
|
111
|
+
|
|
112
|
+
resources = extract_resources_from_routes(routes_file)
|
|
113
|
+
return if resources.empty?
|
|
114
|
+
|
|
115
|
+
puts "\n Detected existing resources: #{resources.join(', ')}" unless @quiet
|
|
116
|
+
puts ' Generating views...' unless @quiet
|
|
117
|
+
|
|
118
|
+
require_relative 'views_command'
|
|
119
|
+
resources.each do |resource_name|
|
|
120
|
+
fields = ViewsCommand.read_schema_fields(resource_name)
|
|
121
|
+
ViewsCommand.new(resource_name, fields, force: true).generate
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def extract_resources_from_routes(routes_file)
|
|
126
|
+
content = File.read(routes_file)
|
|
127
|
+
content.scan(/resources\s+:(\w+)/).flatten.uniq
|
|
128
|
+
end
|
|
129
|
+
|
|
107
130
|
def copy_template(src_dir, dest_dir)
|
|
108
131
|
Dir.glob("#{src_dir}/**/*", File::FNM_DOTMATCH).each do |src|
|
|
109
132
|
next if File.directory?(src)
|
|
@@ -478,14 +478,16 @@ module Belt
|
|
|
478
478
|
puts " update #{schema_file}"
|
|
479
479
|
end
|
|
480
480
|
|
|
481
|
-
# Map generator field types to
|
|
481
|
+
# Map generator field types to schema DSL types.
|
|
482
|
+
# Preserves :text, :date, :datetime so views can render appropriate form inputs.
|
|
482
483
|
def schema_type_for(field_type)
|
|
483
484
|
case field_type.to_s
|
|
484
|
-
when 'text' then '
|
|
485
|
+
when 'text' then 'text'
|
|
485
486
|
when 'integer' then 'integer'
|
|
486
487
|
when 'float' then 'number'
|
|
487
488
|
when 'boolean' then 'boolean'
|
|
488
|
-
when 'date'
|
|
489
|
+
when 'date' then 'date'
|
|
490
|
+
when 'datetime' then 'datetime'
|
|
489
491
|
else 'string'
|
|
490
492
|
end
|
|
491
493
|
end
|
|
@@ -505,7 +507,7 @@ module Belt
|
|
|
505
507
|
return unless Dir.exist?('frontend/src')
|
|
506
508
|
return if @skip_views
|
|
507
509
|
|
|
508
|
-
Belt::CLI::ViewsCommand.new(@name, @fields).generate
|
|
510
|
+
Belt::CLI::ViewsCommand.new(@name, @fields, force: @force).generate
|
|
509
511
|
end
|
|
510
512
|
end
|
|
511
513
|
end
|
|
@@ -0,0 +1,634 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'open3'
|
|
5
|
+
|
|
6
|
+
module Belt
|
|
7
|
+
module CLI
|
|
8
|
+
class LogsCommand
|
|
9
|
+
include AppDetection
|
|
10
|
+
|
|
11
|
+
COLORS = {
|
|
12
|
+
red: "\e[0;31m",
|
|
13
|
+
green: "\e[0;32m",
|
|
14
|
+
yellow: "\e[0;33m",
|
|
15
|
+
blue: "\e[0;34m",
|
|
16
|
+
magenta: "\e[0;35m",
|
|
17
|
+
cyan: "\e[0;36m",
|
|
18
|
+
gray: "\e[0;90m",
|
|
19
|
+
bold: "\e[1m",
|
|
20
|
+
dim: "\e[2m",
|
|
21
|
+
reset: "\e[0m"
|
|
22
|
+
}.freeze
|
|
23
|
+
|
|
24
|
+
def self.run(args)
|
|
25
|
+
if args.include?('--help') || args.include?('-h')
|
|
26
|
+
puts usage
|
|
27
|
+
exit 0
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
new(args).run
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.usage
|
|
34
|
+
<<~USAGE
|
|
35
|
+
Usage: belt logs [lambda] [options]
|
|
36
|
+
|
|
37
|
+
View Lambda function logs. Without arguments, tails all Lambdas for the current environment.
|
|
38
|
+
|
|
39
|
+
Arguments:
|
|
40
|
+
lambda Lambda function short name (e.g., api, worker). Optional — shows all if omitted.
|
|
41
|
+
|
|
42
|
+
Options:
|
|
43
|
+
-e, --env ENV Environment (default: BELT_ENV or first detected)
|
|
44
|
+
-f, --follow Follow logs in real-time
|
|
45
|
+
-s, --since PERIOD Time range (default: 5m). Examples: 5m, 30m, 1h, 2h
|
|
46
|
+
-l, --level LEVEL Minimum log level: DEBUG, INFO, WARN, ERROR (default: INFO)
|
|
47
|
+
--error Show only the most recent error and exit
|
|
48
|
+
--raw Show raw JSON logs without formatting
|
|
49
|
+
--no-color Disable colorized output
|
|
50
|
+
-h, --help Show this help
|
|
51
|
+
|
|
52
|
+
Examples:
|
|
53
|
+
belt logs # Last 5m of all lambdas (current env)
|
|
54
|
+
belt logs api # Last 5m of api lambda
|
|
55
|
+
belt logs api -f # Follow api lambda logs
|
|
56
|
+
belt logs -e prod # Last 5m of all lambdas in prod
|
|
57
|
+
belt logs api -s 30m # Last 30 minutes
|
|
58
|
+
belt logs --error # Show most recent error across all lambdas
|
|
59
|
+
belt logs api --error # Show most recent error for api lambda
|
|
60
|
+
USAGE
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def initialize(args)
|
|
64
|
+
@lambda_name = nil
|
|
65
|
+
@env = nil
|
|
66
|
+
@follow = false
|
|
67
|
+
@since = '5m'
|
|
68
|
+
@level = 'INFO'
|
|
69
|
+
@error_mode = false
|
|
70
|
+
@raw = false
|
|
71
|
+
@color = $stdout.tty?
|
|
72
|
+
parse_args(args)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def run
|
|
76
|
+
@env ||= detect_environment
|
|
77
|
+
abort 'Error: Cannot determine environment. Pass -e ENV or set BELT_ENV.' unless @env
|
|
78
|
+
|
|
79
|
+
@app_name = detect_app_name
|
|
80
|
+
abort 'Error: Cannot determine app name.' unless @app_name
|
|
81
|
+
|
|
82
|
+
if @lambda_name
|
|
83
|
+
tail_single(@lambda_name)
|
|
84
|
+
else
|
|
85
|
+
tail_all
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
def parse_args(args)
|
|
92
|
+
i = 0
|
|
93
|
+
while i < args.length
|
|
94
|
+
case args[i]
|
|
95
|
+
when '-e', '--env'
|
|
96
|
+
@env = args[i + 1]
|
|
97
|
+
i += 2
|
|
98
|
+
when '-f', '--follow'
|
|
99
|
+
@follow = true
|
|
100
|
+
i += 1
|
|
101
|
+
when '-s', '--since'
|
|
102
|
+
@since = args[i + 1]
|
|
103
|
+
i += 2
|
|
104
|
+
when '-l', '--level'
|
|
105
|
+
@level = args[i + 1]&.upcase
|
|
106
|
+
i += 2
|
|
107
|
+
when '--error'
|
|
108
|
+
@error_mode = true
|
|
109
|
+
i += 1
|
|
110
|
+
when '--raw'
|
|
111
|
+
@raw = true
|
|
112
|
+
i += 1
|
|
113
|
+
when '--no-color'
|
|
114
|
+
@color = false
|
|
115
|
+
i += 1
|
|
116
|
+
else
|
|
117
|
+
@lambda_name = args[i] unless args[i].start_with?('-')
|
|
118
|
+
i += 1
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def detect_environment
|
|
124
|
+
ENV.fetch('BELT_ENV', nil) || detect_environments.first
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def tail_single(lambda_name)
|
|
128
|
+
log_group = log_group_for(lambda_name)
|
|
129
|
+
|
|
130
|
+
unless log_group_exists?(log_group)
|
|
131
|
+
abort "#{c(:red)}✗ No log group found: #{log_group}#{c(:reset)}\n " \
|
|
132
|
+
'The Lambda may not have been deployed yet.'
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
if @error_mode
|
|
136
|
+
find_recent_error(log_group, lambda_name)
|
|
137
|
+
elsif @follow
|
|
138
|
+
follow_logs(log_group, lambda_name)
|
|
139
|
+
else
|
|
140
|
+
fetch_historical(log_group, lambda_name)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def tail_all
|
|
145
|
+
lambdas = discover_lambda_names
|
|
146
|
+
if lambdas.empty?
|
|
147
|
+
abort "#{c(:red)}✗ No Lambda functions found for #{@app_name}-#{@env}#{c(:reset)}\n " \
|
|
148
|
+
"Deploy with `belt deploy #{@env}` first, or specify a lambda name: `belt logs api`"
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
if @error_mode
|
|
152
|
+
find_errors_across(lambdas)
|
|
153
|
+
elsif @follow
|
|
154
|
+
follow_all(lambdas)
|
|
155
|
+
else
|
|
156
|
+
fetch_all_historical(lambdas)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def discover_lambda_names
|
|
161
|
+
names = lambda_names_from_terraform
|
|
162
|
+
return names if names.any?
|
|
163
|
+
|
|
164
|
+
lambda_names_from_log_groups
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def lambda_names_from_terraform
|
|
168
|
+
infra_dir = find_infra_dir
|
|
169
|
+
env_dir = infra_dir ? File.join(infra_dir, @env) : nil
|
|
170
|
+
return [] unless env_dir && Dir.exist?(File.join(env_dir, '.terraform'))
|
|
171
|
+
|
|
172
|
+
Dir.chdir(env_dir) do
|
|
173
|
+
output, status = Open3.capture2('terraform', 'output', '-json')
|
|
174
|
+
return [] unless status.success?
|
|
175
|
+
|
|
176
|
+
data = begin
|
|
177
|
+
JSON.parse(output)
|
|
178
|
+
rescue JSON::ParserError
|
|
179
|
+
{}
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
if data['lambda_functions']
|
|
183
|
+
funcs = data['lambda_functions']['value']
|
|
184
|
+
if funcs.is_a?(Hash)
|
|
185
|
+
return funcs.keys
|
|
186
|
+
elsif funcs.is_a?(Array)
|
|
187
|
+
return funcs.map { |f| f.is_a?(String) ? f.split('-').last : nil }.compact
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
data.keys.grep(/_function_name$/).map { |k| data[k]['value']&.split('-')&.last }.compact
|
|
192
|
+
end
|
|
193
|
+
rescue StandardError
|
|
194
|
+
[]
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def lambda_names_from_log_groups
|
|
198
|
+
prefix = "/aws/lambda/#{@app_name}-#{@env}-"
|
|
199
|
+
output, status = Open3.capture2(
|
|
200
|
+
'aws', 'logs', 'describe-log-groups',
|
|
201
|
+
'--log-group-name-prefix', prefix,
|
|
202
|
+
'--query', 'logGroups[].logGroupName',
|
|
203
|
+
'--output', 'json'
|
|
204
|
+
)
|
|
205
|
+
return [] unless status.success?
|
|
206
|
+
|
|
207
|
+
groups = begin
|
|
208
|
+
JSON.parse(output)
|
|
209
|
+
rescue JSON::ParserError
|
|
210
|
+
[]
|
|
211
|
+
end
|
|
212
|
+
groups.map { |g| g.sub(prefix, '') }
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def log_group_for(lambda_name)
|
|
216
|
+
"/aws/lambda/#{@app_name}-#{@env}-#{lambda_name}"
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def log_group_exists?(log_group)
|
|
220
|
+
_, status = Open3.capture2(
|
|
221
|
+
'aws', 'logs', 'describe-log-groups',
|
|
222
|
+
'--log-group-name-prefix', log_group,
|
|
223
|
+
'--query', "logGroups[?logGroupName=='#{log_group}'].logGroupName",
|
|
224
|
+
'--output', 'text'
|
|
225
|
+
)
|
|
226
|
+
status.success?
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def follow_logs(log_group, lambda_name)
|
|
230
|
+
print_header(lambda_name)
|
|
231
|
+
puts "#{c(:yellow)}Following logs (Ctrl+C to stop)...#{c(:reset)}\n\n"
|
|
232
|
+
|
|
233
|
+
cmd = ['aws', 'logs', 'tail', log_group, '--follow', '--format', 'short']
|
|
234
|
+
IO.popen(cmd, err: %i[child out]) do |io|
|
|
235
|
+
io.each_line { |line| process_tail_line(line) }
|
|
236
|
+
end
|
|
237
|
+
rescue Interrupt
|
|
238
|
+
puts "\n#{c(:dim)}Stopped.#{c(:reset)}"
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def follow_all(lambdas)
|
|
242
|
+
puts "#{c(:cyan)}═══════════════════════════════════════════════════════════════#{c(:reset)}"
|
|
243
|
+
puts "#{c(:bold)}Lambda Logs: #{c(:magenta)}#{@app_name}-#{@env}#{c(:reset)} (#{lambdas.join(', ')})"
|
|
244
|
+
puts "#{c(:cyan)}═══════════════════════════════════════════════════════════════#{c(:reset)}"
|
|
245
|
+
puts "#{c(:yellow)}Following logs (Ctrl+C to stop)...#{c(:reset)}\n\n"
|
|
246
|
+
|
|
247
|
+
threads = lambdas.map do |name|
|
|
248
|
+
log_group = log_group_for(name)
|
|
249
|
+
Thread.new do
|
|
250
|
+
cmd = ['aws', 'logs', 'tail', log_group, '--follow', '--format', 'short']
|
|
251
|
+
IO.popen(cmd, err: %i[child out]) do |io|
|
|
252
|
+
io.each_line { |line| process_tail_line(line, prefix: name) }
|
|
253
|
+
end
|
|
254
|
+
rescue StandardError
|
|
255
|
+
nil
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
threads.each(&:join)
|
|
260
|
+
rescue Interrupt
|
|
261
|
+
puts "\n#{c(:dim)}Stopped.#{c(:reset)}"
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def fetch_historical(log_group, lambda_name)
|
|
265
|
+
print_header(lambda_name)
|
|
266
|
+
puts "#{c(:dim)}Fetching last #{@since} of logs...#{c(:reset)}\n\n"
|
|
267
|
+
|
|
268
|
+
events = fetch_log_events(log_group)
|
|
269
|
+
if events.empty?
|
|
270
|
+
puts "#{c(:yellow)}No logs found in the last #{@since}#{c(:reset)}"
|
|
271
|
+
return
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
puts "#{c(:dim)}Found #{events.length} log events#{c(:reset)}\n\n"
|
|
275
|
+
events.each { |event| process_event(event) }
|
|
276
|
+
|
|
277
|
+
puts "\n#{c(:cyan)}═══════════════════════════════════════════════════════════════#{c(:reset)}"
|
|
278
|
+
puts "#{c(:dim)}Tip: Use -f to follow logs in real-time#{c(:reset)}"
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def fetch_all_historical(lambdas)
|
|
282
|
+
puts "#{c(:cyan)}═══════════════════════════════════════════════════════════════#{c(:reset)}"
|
|
283
|
+
puts "#{c(:bold)}Lambda Logs: #{c(:magenta)}#{@app_name}-#{@env}#{c(:reset)} (#{lambdas.join(', ')})"
|
|
284
|
+
puts "#{c(:cyan)}═══════════════════════════════════════════════════════════════#{c(:reset)}"
|
|
285
|
+
puts "#{c(:dim)}Fetching last #{@since} of logs...#{c(:reset)}\n\n"
|
|
286
|
+
|
|
287
|
+
all_events = []
|
|
288
|
+
lambdas.each do |name|
|
|
289
|
+
log_group = log_group_for(name)
|
|
290
|
+
events = fetch_log_events(log_group)
|
|
291
|
+
events.each { |e| e['_lambda'] = name }
|
|
292
|
+
all_events.concat(events)
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
all_events.sort_by! { |e| e['timestamp'] || 0 }
|
|
296
|
+
|
|
297
|
+
if all_events.empty?
|
|
298
|
+
puts "#{c(:yellow)}No logs found in the last #{@since}#{c(:reset)}"
|
|
299
|
+
return
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
puts "#{c(:dim)}Found #{all_events.length} log events#{c(:reset)}\n\n"
|
|
303
|
+
all_events.each { |event| process_event(event, prefix: event['_lambda']) }
|
|
304
|
+
|
|
305
|
+
puts "\n#{c(:cyan)}═══════════════════════════════════════════════════════════════#{c(:reset)}"
|
|
306
|
+
puts "#{c(:dim)}Tip: Use -f to follow logs in real-time#{c(:reset)}"
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def find_recent_error(log_group, lambda_name)
|
|
310
|
+
events = fetch_log_events(log_group, since: '30m', limit: 500)
|
|
311
|
+
error = find_error_in_events(events)
|
|
312
|
+
|
|
313
|
+
if error
|
|
314
|
+
puts "#{c(:bold)}Most recent error in #{c(:magenta)}#{lambda_name}#{c(:reset)}:\n\n"
|
|
315
|
+
format_error_event(error)
|
|
316
|
+
else
|
|
317
|
+
puts "#{c(:green)}✓ No errors found in the last 30 minutes for #{lambda_name}#{c(:reset)}"
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def find_errors_across(lambdas)
|
|
322
|
+
latest_error = nil
|
|
323
|
+
latest_lambda = nil
|
|
324
|
+
|
|
325
|
+
lambdas.each do |name|
|
|
326
|
+
log_group = log_group_for(name)
|
|
327
|
+
events = fetch_log_events(log_group, since: '30m', limit: 500)
|
|
328
|
+
error = find_error_in_events(events)
|
|
329
|
+
next unless error
|
|
330
|
+
|
|
331
|
+
if latest_error.nil? || (error['timestamp'] || 0) > (latest_error['timestamp'] || 0)
|
|
332
|
+
latest_error = error
|
|
333
|
+
latest_lambda = name
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
if latest_error
|
|
338
|
+
puts "#{c(:bold)}Most recent error in #{c(:magenta)}#{latest_lambda}#{c(:reset)}:\n\n"
|
|
339
|
+
format_error_event(latest_error)
|
|
340
|
+
else
|
|
341
|
+
puts "#{c(:green)}✓ No errors found in the last 30 minutes#{c(:reset)}"
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def find_error_in_events(events)
|
|
346
|
+
events.reverse_each do |event|
|
|
347
|
+
msg = event['message'] || ''
|
|
348
|
+
json = parse_json(msg)
|
|
349
|
+
next unless json
|
|
350
|
+
|
|
351
|
+
return event if json['errorMessage'] && json['errorType'] && json['stackTrace']
|
|
352
|
+
return event if json['level'] == 'ERROR'
|
|
353
|
+
return event if json['status_code'].to_i >= 500
|
|
354
|
+
end
|
|
355
|
+
nil
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def format_error_event(event)
|
|
359
|
+
msg = event['message'] || ''
|
|
360
|
+
json = parse_json(msg)
|
|
361
|
+
return puts(msg) unless json
|
|
362
|
+
|
|
363
|
+
timestamp = format_timestamp(event['timestamp'])
|
|
364
|
+
|
|
365
|
+
if json['errorMessage'] && json['errorType']
|
|
366
|
+
format_init_error(json, timestamp)
|
|
367
|
+
else
|
|
368
|
+
format_structured_log(json, timestamp)
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def format_init_error(json, timestamp)
|
|
373
|
+
puts "#{c(:gray)}#{timestamp}#{c(:reset)} #{c(:red)}#{c(:bold)}INIT ERROR#{c(:reset)}"
|
|
374
|
+
puts " #{c(:dim)}Type:#{c(:reset)} #{c(:red)}#{json['errorType']}#{c(:reset)}"
|
|
375
|
+
puts " #{c(:dim)}Message:#{c(:reset)} #{json['errorMessage']}"
|
|
376
|
+
return unless json['stackTrace'].is_a?(Array)
|
|
377
|
+
|
|
378
|
+
puts " #{c(:dim)}Stack:#{c(:reset)}"
|
|
379
|
+
json['stackTrace'].first(15).each do |line|
|
|
380
|
+
if line.match?(%r{(controllers|models|lib|helpers)/})
|
|
381
|
+
puts " #{c(:yellow)}→#{c(:reset)} #{line}"
|
|
382
|
+
elsif line.include?('/var/task/')
|
|
383
|
+
puts " #{c(:cyan)}→#{c(:reset)} #{line}"
|
|
384
|
+
else
|
|
385
|
+
puts " #{c(:gray)} #{line}#{c(:reset)}"
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
def fetch_log_events(log_group, since: @since, limit: 1000)
|
|
391
|
+
duration_ms = parse_since(since)
|
|
392
|
+
start_time = (Time.now.to_i * 1000) - duration_ms
|
|
393
|
+
|
|
394
|
+
output, status = Open3.capture2(
|
|
395
|
+
'aws', 'logs', 'filter-log-events',
|
|
396
|
+
'--log-group-name', log_group,
|
|
397
|
+
'--start-time', start_time.to_s,
|
|
398
|
+
'--limit', limit.to_s,
|
|
399
|
+
'--output', 'json'
|
|
400
|
+
)
|
|
401
|
+
return [] unless status.success?
|
|
402
|
+
|
|
403
|
+
data = begin
|
|
404
|
+
JSON.parse(output)
|
|
405
|
+
rescue JSON::ParserError
|
|
406
|
+
{}
|
|
407
|
+
end
|
|
408
|
+
data['events'] || []
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
def process_tail_line(line, prefix: nil)
|
|
412
|
+
return if line.match?(/\b(START|END|REPORT|INIT_START|INIT_REPORT)\s/)
|
|
413
|
+
return if line.include?('[LambdaLoadout') || line.include?('"_aws":')
|
|
414
|
+
|
|
415
|
+
if line.include?('Critical exception from handler')
|
|
416
|
+
prefix_str = prefix ? "#{c(:blue)}[#{prefix}]#{c(:reset)} " : ''
|
|
417
|
+
puts "#{prefix_str}#{c(:red)}#{c(:bold)}CRITICAL EXCEPTION#{c(:reset)}"
|
|
418
|
+
return
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
json_part = line.sub(/\A[\dT:.+\-Z ]+\s*/, '').strip
|
|
422
|
+
json = parse_json(json_part)
|
|
423
|
+
|
|
424
|
+
if json
|
|
425
|
+
if json['errorMessage'] && json['errorType'] && json['stackTrace']
|
|
426
|
+
format_aws_error(json, prefix: prefix)
|
|
427
|
+
elsif @raw
|
|
428
|
+
puts json_part
|
|
429
|
+
else
|
|
430
|
+
timestamp = extract_time_from_line(line)
|
|
431
|
+
format_structured_log(json, timestamp, prefix: prefix)
|
|
432
|
+
end
|
|
433
|
+
elsif line.strip.length.positive?
|
|
434
|
+
prefix_str = prefix ? "#{c(:blue)}[#{prefix}]#{c(:reset)} " : ''
|
|
435
|
+
puts "#{prefix_str}#{c(:gray)}#{line.strip}#{c(:reset)}"
|
|
436
|
+
end
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
def process_event(event, prefix: nil)
|
|
440
|
+
msg = event['message'] || ''
|
|
441
|
+
|
|
442
|
+
return if msg.match?(/\A(START|END|REPORT|INIT_START|INIT_REPORT)\s/)
|
|
443
|
+
return if msg.include?('[LambdaLoadout') || msg.include?('"_aws":')
|
|
444
|
+
|
|
445
|
+
if msg.include?('Critical exception from handler')
|
|
446
|
+
prefix_str = prefix ? "#{c(:blue)}[#{prefix}]#{c(:reset)} " : ''
|
|
447
|
+
puts "#{prefix_str}#{c(:red)}#{c(:bold)}CRITICAL EXCEPTION#{c(:reset)}"
|
|
448
|
+
return
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
json = parse_json(msg)
|
|
452
|
+
timestamp = format_timestamp(event['timestamp'])
|
|
453
|
+
|
|
454
|
+
if json
|
|
455
|
+
if json['errorMessage'] && json['errorType'] && json['stackTrace']
|
|
456
|
+
format_aws_error(json, prefix: prefix)
|
|
457
|
+
elsif @raw
|
|
458
|
+
puts JSON.pretty_generate(json)
|
|
459
|
+
else
|
|
460
|
+
format_structured_log(json, timestamp, prefix: prefix)
|
|
461
|
+
end
|
|
462
|
+
elsif msg.strip.length.positive?
|
|
463
|
+
prefix_str = prefix ? "#{c(:blue)}[#{prefix}]#{c(:reset)} " : ''
|
|
464
|
+
puts "#{prefix_str}#{c(:gray)}#{msg.strip}#{c(:reset)}"
|
|
465
|
+
end
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
def format_structured_log(json, timestamp, prefix: nil)
|
|
469
|
+
level = json['level']
|
|
470
|
+
message = json['message'] || ''
|
|
471
|
+
|
|
472
|
+
return unless should_show_level?(level)
|
|
473
|
+
|
|
474
|
+
prefix_str = prefix ? "#{c(:blue)}[#{prefix}]#{c(:reset)} " : ''
|
|
475
|
+
|
|
476
|
+
case level
|
|
477
|
+
when 'ERROR'
|
|
478
|
+
format_error_log(json, message, timestamp, prefix_str)
|
|
479
|
+
when 'WARN'
|
|
480
|
+
format_warn_log(json, message, timestamp, prefix_str)
|
|
481
|
+
else
|
|
482
|
+
format_info_log(json, message, timestamp, prefix_str)
|
|
483
|
+
end
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
def format_error_log(json, message, timestamp, prefix_str)
|
|
487
|
+
puts "#{prefix_str}#{c(:gray)}#{timestamp}#{c(:reset)} " \
|
|
488
|
+
"#{c(:red)}ERROR#{c(:reset)} #{c(:bold)}#{message}#{c(:reset)}"
|
|
489
|
+
puts " #{c(:dim)}Action:#{c(:reset)} #{json['action']}" if json['action']
|
|
490
|
+
if json['error_class']
|
|
491
|
+
puts " #{c(:dim)}Error:#{c(:reset)} " \
|
|
492
|
+
"#{c(:red)}#{json['error_class']}#{c(:reset)}: #{json['error_message']}"
|
|
493
|
+
end
|
|
494
|
+
puts " #{c(:dim)}Path:#{c(:reset)} #{json['path']}" if json['path']
|
|
495
|
+
format_backtrace(json['backtrace'])
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
def format_warn_log(json, message, timestamp, prefix_str)
|
|
499
|
+
puts "#{prefix_str}#{c(:gray)}#{timestamp}#{c(:reset)} #{c(:yellow)}WARN#{c(:reset)} #{message}"
|
|
500
|
+
if json['error_class']
|
|
501
|
+
puts " #{c(:dim)}Error:#{c(:reset)} " \
|
|
502
|
+
"#{c(:red)}#{json['error_class']}#{c(:reset)}: #{json['error_message']}"
|
|
503
|
+
end
|
|
504
|
+
puts " #{c(:dim)}Path:#{c(:reset)} #{json['path']}" if json['path']
|
|
505
|
+
format_backtrace(json['backtrace'])
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
def format_info_log(json, message, timestamp, prefix_str)
|
|
509
|
+
case message
|
|
510
|
+
when 'Lambda invoked'
|
|
511
|
+
path = strip_namespace(json['path'])
|
|
512
|
+
puts "#{prefix_str}#{c(:gray)}#{timestamp}#{c(:reset)} " \
|
|
513
|
+
"#{c(:cyan)}Started#{c(:reset)} #{c(:bold)}#{json['http_method']}#{c(:reset)} #{path}"
|
|
514
|
+
when 'Request completed'
|
|
515
|
+
path = strip_namespace(json['path'])
|
|
516
|
+
sc = json['status_code'].to_i
|
|
517
|
+
sc_color = status_color(sc)
|
|
518
|
+
puts "#{prefix_str}#{c(:gray)}#{timestamp}#{c(:reset)} " \
|
|
519
|
+
"#{c(:cyan)}Completed#{c(:reset)} #{c(sc_color)}#{sc}#{c(:reset)} #{path}"
|
|
520
|
+
else
|
|
521
|
+
level = json['level']
|
|
522
|
+
level_col = level_color_for(level)
|
|
523
|
+
puts "#{prefix_str}#{c(:gray)}#{timestamp}#{c(:reset)} #{level_col}#{level}#{c(:reset)} #{message}" if level
|
|
524
|
+
end
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
def format_aws_error(json, prefix: nil)
|
|
528
|
+
prefix_str = prefix ? "#{c(:blue)}[#{prefix}]#{c(:reset)} " : ''
|
|
529
|
+
puts "#{prefix_str} #{c(:dim)}Error Type:#{c(:reset)} #{c(:red)}#{json['errorType']}#{c(:reset)}"
|
|
530
|
+
puts "#{prefix_str} #{c(:dim)}Error Message:#{c(:reset)} #{json['errorMessage']}"
|
|
531
|
+
return unless json['stackTrace'].is_a?(Array)
|
|
532
|
+
|
|
533
|
+
puts "#{prefix_str} #{c(:dim)}Stack Trace:#{c(:reset)}"
|
|
534
|
+
json['stackTrace'].first(20).each do |line|
|
|
535
|
+
if line.match?(%r{(controllers|models|lib|helpers)/})
|
|
536
|
+
puts "#{prefix_str} #{c(:yellow)}→#{c(:reset)} #{line}"
|
|
537
|
+
elsif line.include?('/var/task/')
|
|
538
|
+
puts "#{prefix_str} #{c(:cyan)}→#{c(:reset)} #{line}"
|
|
539
|
+
else
|
|
540
|
+
puts "#{prefix_str} #{c(:gray)} #{line}#{c(:reset)}"
|
|
541
|
+
end
|
|
542
|
+
end
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
def format_backtrace(backtrace)
|
|
546
|
+
return unless backtrace.is_a?(Array) && backtrace.any?
|
|
547
|
+
|
|
548
|
+
puts " #{c(:dim)}Backtrace:#{c(:reset)}"
|
|
549
|
+
backtrace.first(15).each do |line|
|
|
550
|
+
if line.match?(%r{(controllers|models|lib|helpers)/})
|
|
551
|
+
puts " #{c(:yellow)}→#{c(:reset)} #{line}"
|
|
552
|
+
else
|
|
553
|
+
puts " #{c(:gray)} #{line}#{c(:reset)}"
|
|
554
|
+
end
|
|
555
|
+
end
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
def should_show_level?(level)
|
|
559
|
+
return true unless level
|
|
560
|
+
|
|
561
|
+
levels = { 'DEBUG' => 1, 'INFO' => 2, 'WARN' => 3, 'ERROR' => 4 }
|
|
562
|
+
(levels[level] || 0) >= (levels[@level] || 2)
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
def level_color_for(level)
|
|
566
|
+
case level
|
|
567
|
+
when 'INFO' then c(:green)
|
|
568
|
+
when 'WARN' then c(:yellow)
|
|
569
|
+
when 'ERROR' then c(:red)
|
|
570
|
+
else c(:gray)
|
|
571
|
+
end
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
def status_color(code)
|
|
575
|
+
if code >= 500
|
|
576
|
+
:red
|
|
577
|
+
elsif code >= 400
|
|
578
|
+
:yellow
|
|
579
|
+
else
|
|
580
|
+
:green
|
|
581
|
+
end
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
def strip_namespace(path)
|
|
585
|
+
return path unless path
|
|
586
|
+
|
|
587
|
+
path.sub(%r{\A/[^/]+}, '')
|
|
588
|
+
end
|
|
589
|
+
|
|
590
|
+
def print_header(lambda_name)
|
|
591
|
+
full_name = "#{@app_name}-#{@env}-#{lambda_name}"
|
|
592
|
+
puts "#{c(:cyan)}═══════════════════════════════════════════════════════════════#{c(:reset)}"
|
|
593
|
+
puts "#{c(:bold)}Lambda Logs: #{c(:magenta)}#{full_name}#{c(:reset)}"
|
|
594
|
+
puts "#{c(:cyan)}═══════════════════════════════════════════════════════════════#{c(:reset)}"
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
def format_timestamp(epoch_ms)
|
|
598
|
+
return '' unless epoch_ms
|
|
599
|
+
|
|
600
|
+
Time.at(epoch_ms / 1000.0).strftime('%H:%M:%S')
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
def extract_time_from_line(line)
|
|
604
|
+
match = line.match(/\A(\d{4}-\d{2}-\d{2}T[\d:]+)/)
|
|
605
|
+
match ? match[1].split('T').last : ''
|
|
606
|
+
end
|
|
607
|
+
|
|
608
|
+
def parse_since(value)
|
|
609
|
+
num = value.to_i
|
|
610
|
+
case value
|
|
611
|
+
when /h\z/ then num * 60 * 60 * 1000
|
|
612
|
+
when /m\z/ then num * 60 * 1000
|
|
613
|
+
when /s\z/ then num * 1000
|
|
614
|
+
else num * 60 * 1000
|
|
615
|
+
end
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
def parse_json(str)
|
|
619
|
+
JSON.parse(str)
|
|
620
|
+
rescue StandardError
|
|
621
|
+
nil
|
|
622
|
+
end
|
|
623
|
+
|
|
624
|
+
def find_infra_dir
|
|
625
|
+
candidates = %w[infrastructure infra]
|
|
626
|
+
candidates.map { |d| File.join(Dir.pwd, d) }.find { |d| Dir.exist?(d) }
|
|
627
|
+
end
|
|
628
|
+
|
|
629
|
+
def c(name)
|
|
630
|
+
@color ? COLORS[name].to_s : ''
|
|
631
|
+
end
|
|
632
|
+
end
|
|
633
|
+
end
|
|
634
|
+
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'
|
|
@@ -28,7 +32,7 @@ module Belt
|
|
|
28
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)
|
|
@@ -45,20 +49,37 @@ module Belt
|
|
|
45
49
|
|
|
46
50
|
# Extract fields from model block
|
|
47
51
|
if content =~ /model :#{singular} do\n(.*?)\n\s*end/m
|
|
48
|
-
::Regexp.last_match(1)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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 }
|
|
53
72
|
end
|
|
54
73
|
else
|
|
55
74
|
[]
|
|
56
75
|
end
|
|
57
76
|
end
|
|
58
77
|
|
|
59
|
-
def initialize(name, fields)
|
|
78
|
+
def initialize(name, fields, force: false)
|
|
60
79
|
@name = name.downcase.gsub(/[^a-z0-9_]/, '_')
|
|
61
80
|
@fields = fields
|
|
81
|
+
@force = force
|
|
82
|
+
@overwrite_all = false
|
|
62
83
|
@singular_name = Belt::Inflector.singularize(@name)
|
|
63
84
|
@resource_name = Belt::Inflector.pluralize(@singular_name)
|
|
64
85
|
@class_name = Belt::Inflector.classify(@singular_name)
|
|
@@ -97,8 +118,61 @@ module Belt
|
|
|
97
118
|
def write_template(template_name, dest_path)
|
|
98
119
|
template_path = File.join(TEMPLATE_DIR, template_name)
|
|
99
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
|
+
|
|
100
140
|
File.write(dest_path, content)
|
|
101
|
-
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
|
|
102
176
|
end
|
|
103
177
|
|
|
104
178
|
def inject_routes
|
|
@@ -109,6 +183,12 @@ module Belt
|
|
|
109
183
|
pages_dir = @resource_name
|
|
110
184
|
plural_class = @plural_class_name || Belt::Inflector.camelize(@resource_name)
|
|
111
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
|
+
|
|
112
192
|
import_lines = [
|
|
113
193
|
"import #{plural_class}Index from './pages/#{pages_dir}/#{plural_class}Index'",
|
|
114
194
|
"import #{@class_name}Show from './pages/#{pages_dir}/#{@class_name}Show'",
|
data/lib/belt/cli.rb
CHANGED
|
@@ -22,6 +22,7 @@ require_relative 'cli/contracts_command'
|
|
|
22
22
|
require_relative 'cli/lambda_config_command'
|
|
23
23
|
require_relative 'cli/tasks_command'
|
|
24
24
|
require_relative 'cli/console_command'
|
|
25
|
+
require_relative 'cli/logs_command'
|
|
25
26
|
require_relative 'cli/doctor_command'
|
|
26
27
|
require_relative 'cli/plugin_command'
|
|
27
28
|
|
|
@@ -35,6 +36,7 @@ module Belt
|
|
|
35
36
|
'contracts' => Belt::CLI::ContractsCommand,
|
|
36
37
|
'lambda-config' => Belt::CLI::LambdaConfigCommand,
|
|
37
38
|
%w[console c] => Belt::CLI::ConsoleCommand,
|
|
39
|
+
'logs' => Belt::CLI::LogsCommand,
|
|
38
40
|
%w[tasks --tasks -T] => Belt::CLI::TasksCommand,
|
|
39
41
|
'setup' => Belt::CLI::SetupCommand,
|
|
40
42
|
'doctor' => Belt::CLI::DoctorCommand,
|
|
@@ -116,6 +118,7 @@ module Belt
|
|
|
116
118
|
|
|
117
119
|
console Start an interactive console (IRB)
|
|
118
120
|
c Alias for console
|
|
121
|
+
logs [lambda] [-f] [-s 5m] [-e env] View Lambda function logs
|
|
119
122
|
tasks [-g PATTERN] [-a] List available rake tasks
|
|
120
123
|
-T [-g PATTERN] [-a] Alias for tasks
|
|
121
124
|
setup state Create/select S3 state bucket
|
data/lib/belt/route_dsl.rb
CHANGED
|
@@ -520,7 +520,7 @@ module Belt
|
|
|
520
520
|
|
|
521
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
|
@@ -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
|
|
@@ -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
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: belt
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.13
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Stowzilla
|
|
@@ -16,6 +16,9 @@ dependencies:
|
|
|
16
16
|
- - "~>"
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
18
|
version: '0.0'
|
|
19
|
+
- - ">="
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: 0.0.13
|
|
19
22
|
type: :runtime
|
|
20
23
|
prerelease: false
|
|
21
24
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -23,6 +26,9 @@ dependencies:
|
|
|
23
26
|
- - "~>"
|
|
24
27
|
- !ruby/object:Gem::Version
|
|
25
28
|
version: '0.0'
|
|
29
|
+
- - ">="
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: 0.0.13
|
|
26
32
|
- !ruby/object:Gem::Dependency
|
|
27
33
|
name: activesupport
|
|
28
34
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -89,6 +95,7 @@ files:
|
|
|
89
95
|
- lib/belt/cli/generate_command.rb
|
|
90
96
|
- lib/belt/cli/generator_registry.rb
|
|
91
97
|
- lib/belt/cli/lambda_config_command.rb
|
|
98
|
+
- lib/belt/cli/logs_command.rb
|
|
92
99
|
- lib/belt/cli/new_command.rb
|
|
93
100
|
- lib/belt/cli/path_gem_materializer.rb
|
|
94
101
|
- lib/belt/cli/plugin_command.rb
|