belt 0.4.1 → 0.4.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 846db7f1373b5bd98f3e795404b1f157825f5dd94541bd46aa76e3510ba89adf
4
- data.tar.gz: 251135fea6ee2d3d55e7dcdf46b98460ddc3df50fa1e4113b561f721755c935d
3
+ metadata.gz: e17e0755e497e26d8bd10604e6f050b0c6e0b69e3627276f8d83be984862d5da
4
+ data.tar.gz: b4249c4ac11715ae45a091c3eced0d0bad8ecd5d89a750edfaa8586ab6449758
5
5
  SHA512:
6
- metadata.gz: 60b0ecf81e375ea1c219c84ccf7f90d568437cc81507c708ec56a40f903178e0611e5b6be03e1e71185ce66263d1ec0c12ddee8e1a4d2cfdc26f99dba4828c48
7
- data.tar.gz: fb3fa14275a68c7e79892504d0fef2cbe86727225033a7d7bc3c1272b963413b5ed7259c3a5c2f1e202770d16b0234ff242571e4d2b7abed50a040090da250e1
6
+ metadata.gz: abe5b18f7eeeb92bba2863746e7cc71d854078462274993403c4d7b93af81ff2c82a3b5c1db2fbdf537f8a139ec15964d76aad042505ade12ad64e6069f40a5a
7
+ data.tar.gz: dba1d0cca64620fc48b495ee844600e7a8e38c577ad3f27ada191263038f38a081a3d640f9a66e00fd97c138e1686ae91654d6995225d357bec8a5016e903f5e
data/CHANGELOG.md CHANGED
@@ -4,6 +4,18 @@
4
4
 
5
5
  ### Bug Fix
6
6
 
7
+ - **`belt setup tables` no longer silently clobbers hand-added tables/GSIs.**
8
+ Regenerating `dynamodb.tf` is a full overwrite from `lambda/models/*.rb`, so a
9
+ table or `global_secondary_index` added straight into the `.tf` file — one the
10
+ models don't declare — used to vanish on the next run. Dropping a live GSI is
11
+ not harmless: queries against it start failing. The generator now diffs the
12
+ existing file and, when the regen would remove infrastructure it can't
13
+ re-derive, it warns and lists exactly what would disappear. An interactive run
14
+ prompts before overwriting; a generator auto-sync refuses and leaves the file
15
+ untouched. Pass `--force` (`-f`/`--yes`/`-y`) to overwrite anyway. The
16
+ `dynamodb.tf` header comment now spells out this behavior too. (Fixes the
17
+ misleading "harmless diff" framing in the 0.4.0 notes below.)
18
+
7
19
  - **Apply environment AWS profile in `belt deploy frontend`, `belt frontend env`, `belt logs`, and `belt server`**:
8
20
  Standalone frontend deployment (`belt deploy frontend <env>`), frontend env generation (`belt frontend env <env>`),
9
21
  log viewing (`belt logs`), and the local dev server (`belt server`) now load `infrastructure/<env>/belt.rb`
@@ -40,7 +40,7 @@ module Belt
40
40
 
41
41
  Subcommands:
42
42
  state Set up S3 bucket for Terraform state
43
- tables Generate DynamoDB table definitions from contracts.rb
43
+ tables Generate DynamoDB table definitions from lambda/models/*.rb
44
44
  frontend Generate S3 + CloudFront infrastructure for frontend hosting
45
45
 
46
46
  Options for state:
@@ -15,7 +15,12 @@ module Belt
15
15
  # dynamodb.tf lives in infrastructure/modules/app and uses var.environment.
16
16
  EnvResolver.resolve(args)
17
17
 
18
- new.run
18
+ # `--force`/`-y` overwrites dynamodb.tf even when the regen would drop a
19
+ # hand-added table or GSI. Without it, an interactive run prompts before
20
+ # clobbering and a quiet (generator) run refuses.
21
+ force = args.intersect?(%w[--force -f --yes -y])
22
+
23
+ new(force: force).run
19
24
  end
20
25
 
21
26
  # Automatically sync dynamodb.tf in the app module.
@@ -26,8 +31,9 @@ module Belt
26
31
  new(quiet: true).run
27
32
  end
28
33
 
29
- def initialize(quiet: false)
34
+ def initialize(quiet: false, force: false)
30
35
  @quiet = quiet
36
+ @force = force
31
37
  end
32
38
 
33
39
  def run
@@ -166,6 +172,16 @@ module Belt
166
172
  # Skip if content is unchanged
167
173
  return if existing_content == new_content
168
174
 
175
+ # Regenerating dynamodb.tf is a full overwrite. Anything hand-added to the
176
+ # file that the generator can't re-derive from the models — a GSI added
177
+ # straight into the .tf, or a table for a model that no longer exists — would
178
+ # silently vanish. Detect that and refuse (quiet) or prompt (interactive)
179
+ # unless --force was passed.
180
+ if existing_content
181
+ dropped = detect_dropped_infrastructure(existing_content, new_content)
182
+ return if dropped.any? && !safe_to_overwrite?(dest, dropped)
183
+ end
184
+
169
185
  File.write(dest, new_content)
170
186
 
171
187
  if @quiet
@@ -179,10 +195,104 @@ module Belt
179
195
  end
180
196
  end
181
197
 
198
+ # Compare the existing dynamodb.tf against the freshly rendered content and
199
+ # return a list of human-readable descriptions of infrastructure that exists
200
+ # today but wouldn't be regenerated — i.e. would be dropped by the overwrite.
201
+ #
202
+ # We only surface *removals*, since additions and edits are the whole point of
203
+ # re-running the generator. A removal, on the other hand, is usually a mistake:
204
+ # a GSI someone added by hand that the model doesn't declare.
205
+ def detect_dropped_infrastructure(existing_content, new_content)
206
+ dropped = []
207
+
208
+ old_tables = table_labels(existing_content)
209
+ new_tables = table_labels(new_content)
210
+ dropped.concat((old_tables - new_tables).map { |label| "table \"#{label}\"" })
211
+
212
+ # Only compare GSIs on tables that survive — a dropped table already
213
+ # accounts for its indexes, no need to list them twice.
214
+ (old_tables & new_tables).each do |label|
215
+ old_gsis = gsi_names(existing_content, label)
216
+ new_gsis = gsi_names(new_content, label)
217
+ dropped.concat((old_gsis - new_gsis).map { |gsi| "GSI \"#{gsi}\" on table \"#{label}\"" })
218
+ end
219
+
220
+ dropped
221
+ end
222
+
223
+ # Resource labels for every aws_dynamodb_table block in the content.
224
+ def table_labels(content)
225
+ content.scan(/resource\s+"aws_dynamodb_table"\s+"([^"]+)"/).flatten
226
+ end
227
+
228
+ # GSI names declared inside the given table's resource block.
229
+ def gsi_names(content, label)
230
+ block = table_block(content, label)
231
+ return [] unless block
232
+
233
+ block.scan(/global_secondary_index\s*\{[^}]*?name\s*=\s*"([^"]+)"/m).flatten
234
+ end
235
+
236
+ # Extract the body of a single aws_dynamodb_table resource block by brace
237
+ # matching, so we can scope GSI lookups to one table.
238
+ def table_block(content, label)
239
+ marker = /resource\s+"aws_dynamodb_table"\s+"#{Regexp.escape(label)}"\s*\{/
240
+ match = content.match(marker)
241
+ return nil unless match
242
+
243
+ start = match.end(0)
244
+ depth = 1
245
+ idx = start
246
+ while idx < content.length && depth.positive?
247
+ case content[idx]
248
+ when '{' then depth += 1
249
+ when '}' then depth -= 1
250
+ end
251
+ idx += 1
252
+ end
253
+ content[start...(idx - 1)]
254
+ end
255
+
256
+ # Decide whether it's safe to overwrite dynamodb.tf when the regen would drop
257
+ # hand-added infrastructure. Returns true to proceed, false to abort the write.
258
+ def safe_to_overwrite?(dest, dropped)
259
+ return true if @force
260
+
261
+ warn_dropped(dest, dropped)
262
+
263
+ # A generator auto-sync must never silently destroy custom infra. Refuse and
264
+ # tell the user to resolve it deliberately.
265
+ if @quiet
266
+ puts ' ⚠ skipped dynamodb.tf — would drop hand-added infrastructure ' \
267
+ '(run `belt setup tables` to review)'
268
+ return false
269
+ end
270
+
271
+ print "\nOverwrite anyway and drop the above? [y/N] "
272
+ response = $stdin.gets&.strip&.downcase
273
+ return true if %w[y yes].include?(response)
274
+
275
+ puts '✗ Aborted. dynamodb.tf left unchanged.'
276
+ puts ' Move the custom definition into a model, or re-run with --force to overwrite.'
277
+ false
278
+ end
279
+
280
+ def warn_dropped(dest, dropped)
281
+ puts "\n⚠ Regenerating #{dest} would DROP infrastructure not derived from your models:"
282
+ dropped.each { |d| puts " • #{d}" }
283
+ puts "\n This usually means a table or GSI was added to dynamodb.tf by hand."
284
+ puts ' Belt only tracks tables and indexes it can read from lambda/models/*.rb,'
285
+ puts ' so a hand-added definition is invisible to the generator and gets overwritten.'
286
+ end
287
+
182
288
  def render_dynamodb(models)
183
289
  blocks = models.map { |m| render_table(m) }
184
- "# Auto-generated by Belt from model definitions\n" \
185
- "# Do not edit manually — re-run `belt setup tables`\n\n#{blocks.join("\n\n")}\n"
290
+ "# Auto-generated by Belt from model definitions in lambda/models/*.rb\n" \
291
+ "#\n" \
292
+ "# Do NOT edit manually. `belt setup tables` overwrites this whole file from\n" \
293
+ "# your models. Any table or GSI added here by hand (that a model doesn't\n" \
294
+ "# declare) will be DROPPED on the next regeneration. Define indexes on the\n" \
295
+ "# model via indexes(), belongs_to, or cognito_authenticatable instead.\n\n#{blocks.join("\n\n")}\n"
186
296
  end
187
297
 
188
298
  def render_table(model)
data/lib/belt/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Belt
4
- VERSION = '0.4.1'
4
+ VERSION = '0.4.2'
5
5
  end
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.4.1
4
+ version: 0.4.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stowzilla