belt 0.6.7 → 0.6.8

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: ef904a59417a3f019eab7e6339fc6d13290bf05ff3d1d435c434834cea189bc2
4
- data.tar.gz: 93a8820ef48b523d41e7a4aa8d0544090b2472bc7805b96bfee9f0f45c8e8114
3
+ metadata.gz: 5c8fdcdc85f77c9538860db16ba868045f1a4482487ce7cfbf6b3e1921e9c793
4
+ data.tar.gz: 996b9b570963202a4b9acde7fdaa4e83550606a7e6ba9ba89cf20dabff1c5cf7
5
5
  SHA512:
6
- metadata.gz: b562534dbed0dcad13db7d4e6f0ff9ff99f469af4de025a682902375d39ffe0bf73447a749674443acb81632e40f2f2c4b0e34ceced872a37d4a904dd5725b7f
7
- data.tar.gz: d0915177422b65ef6f9a2dfb542cdd63b578acdbae1abf97cc4699354400b046e507818f71f7756a73f54183916921d2ad6862c877c4b8385fc6feb22ebd7b2d
6
+ metadata.gz: 3dbbf13ab635583594d630355888e2197f8a07052348eee39f47a1c328fba39392df21c88f12a8ab34252de920ecf711ba1915785de606a527afc79299ed69e8
7
+ data.tar.gz: e2f3a01545969526003d72e5c0b486e412c852ef8f6cdd6220ad92be5c6771dba8e0e3f5a1880ed691c72ad9e27a5e0caf11a2916717fcdf74c6e81fe05e871b
data/CHANGELOG.md CHANGED
@@ -1,6 +1,21 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 0.6.8
4
+
5
+ ### Added
6
+
7
+ - **`ttl_attribute` model macro.** `ttl_attribute :expires_at` on a model makes
8
+ `belt setup tables` emit `ttl { attribute_name = "expiresAt", enabled = true }`
9
+ for its table. A string is used verbatim. `belt explain models`. (Fizzy #1796)
10
+
11
+ ### Fixed
12
+
13
+ - **`belt setup tables` no longer silently drops `ttl` blocks.** A hand-added `ttl`,
14
+ or any other table block or setting the models don't produce (`stream_enabled`,
15
+ `server_side_encryption`, …), now counts as dropped infrastructure like a
16
+ hand-added GSI. Interactive runs prompt, and generator auto-syncs (`belt g policies`
17
+ and friends) skip the file. Hand-written comments the regen removes are now
18
+ reported instead of vanishing quietly. (Fizzy #1796)
4
19
 
5
20
  ## 0.6.7
6
21
 
@@ -9,6 +9,8 @@ module Belt
9
9
  class TablesCommand
10
10
  MODULE_DIR = 'infrastructure/modules/app'
11
11
  MODELS_DIR = 'lambda/models'
12
+ # Table blocks the drop check compares some other way (see #table_settings).
13
+ DERIVED_BLOCKS = %w[attribute global_secondary_index].freeze
12
14
 
13
15
  def self.run(args)
14
16
  # Environment argument accepted for backwards compatibility but unused —
@@ -99,7 +101,16 @@ module Belt
99
101
  # Deduplicate by index name
100
102
  indexes.uniq! { |idx| idx[:name] }
101
103
 
102
- { name: model_name, indexes: indexes }
104
+ { name: model_name, indexes: indexes, ttl: extract_ttl_attribute(content) }
105
+ end
106
+
107
+ # `ttl_attribute :expires_at` (Belt::Models::TtlMacro). A symbol is camelCased the
108
+ # way ActiveItem writes it; a string is the DynamoDB attribute name verbatim.
109
+ def extract_ttl_attribute(content)
110
+ match = uncommented(content).match(/^\s*ttl_attribute[\s(]+(?::(\w+)|['"]([^'"]+)['"])/)
111
+ return nil unless match
112
+
113
+ match[1] ? Belt::Inflector.camelize_lower(match[1]) : match[2]
103
114
  end
104
115
 
105
116
  def extract_indexes(content)
@@ -180,6 +191,8 @@ module Belt
180
191
  if existing_content
181
192
  dropped = detect_dropped_infrastructure(existing_content, new_content)
182
193
  return if dropped.any? && !safe_to_overwrite?(dest, dropped)
194
+
195
+ warn_dropped_comments(dest, dropped_comments(existing_content, new_content))
183
196
  end
184
197
 
185
198
  File.write(dest, new_content)
@@ -215,11 +228,63 @@ module Belt
215
228
  old_gsis = gsi_names(existing_content, label)
216
229
  new_gsis = gsi_names(new_content, label)
217
230
  dropped.concat((old_gsis - new_gsis).map { |gsi| "GSI \"#{gsi}\" on table \"#{label}\"" })
231
+ dropped.concat(dropped_table_settings(existing_content, new_content, label))
218
232
  end
219
233
 
220
234
  dropped
221
235
  end
222
236
 
237
+ # Nested blocks (`ttl`, `server_side_encryption`, …) and settings (`stream_enabled`,
238
+ # …) a table has today that the regenerated table wouldn't. A hand-added ttl block
239
+ # is the usual one — dropping it turns expiry off and rows quietly pile up.
240
+ def dropped_table_settings(existing_content, new_content, label)
241
+ old_settings = table_settings(table_block(existing_content, label))
242
+ new_settings = table_settings(table_block(new_content, label))
243
+
244
+ (old_settings.keys - new_settings.keys).map do |key|
245
+ "#{old_settings[key]} on table \"#{label}\""
246
+ end
247
+ end
248
+
249
+ # Map of identity → description for every top-level setting in a table body.
250
+ # `attribute` and `global_secondary_index` are compared separately (GSIs) or are
251
+ # derived from them (attributes), so they're left out here.
252
+ def table_settings(block)
253
+ return {} unless block
254
+
255
+ settings = {}
256
+ depth = 0
257
+ block.each_line do |line|
258
+ code = line.sub(/#.*/, '').strip
259
+ if depth.zero?
260
+ if (name = code[/\A(\w+)\s*\{/, 1])
261
+ settings.merge!(block_setting(name, block, line)) unless DERIVED_BLOCKS.include?(name)
262
+ elsif (name = code[/\A(\w+)\s*=/, 1])
263
+ settings[name] = "setting \"#{name}\""
264
+ end
265
+ end
266
+ depth += code.count('{') - code.count('}')
267
+ end
268
+ settings
269
+ end
270
+
271
+ def block_setting(name, block, line)
272
+ return { name => "block \"#{name}\"" } unless name == 'ttl'
273
+
274
+ body = block[block.index(line)..][/\{(.*?)\}/m, 1].to_s
275
+ attr = body[/attribute_name\s*=\s*"([^"]+)"/, 1]
276
+ { "ttl:#{attr}" => "ttl on \"#{attr}\"" }
277
+ end
278
+
279
+ # Hand-written comments can't be re-derived either. They're not infrastructure,
280
+ # so losing them doesn't block the write, but it shouldn't happen silently.
281
+ def dropped_comments(existing_content, new_content)
282
+ # The leading banner is Belt's own and changes between versions — skip it.
283
+ body = existing_content.lines.drop_while { |l| l.strip.empty? || l.lstrip.start_with?('#') }
284
+ body.map(&:strip).select { |l| l.start_with?('#', '//') } -
285
+ new_content.lines.map(&:strip)
286
+ end
287
+
223
288
  # Resource labels for every aws_dynamodb_table block in the content.
224
289
  def table_labels(content)
225
290
  content.scan(/resource\s+"aws_dynamodb_table"\s+"([^"]+)"/).flatten
@@ -280,19 +345,28 @@ module Belt
280
345
  def warn_dropped(dest, dropped)
281
346
  puts "\n⚠ Regenerating #{dest} would DROP infrastructure not derived from your models:"
282
347
  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,'
348
+ puts "\n This usually means a table, GSI, or setting (e.g. ttl) was added to dynamodb.tf by hand."
349
+ puts ' Belt only tracks what it can read from lambda/models/*.rb (indexes, belongs_to,'
350
+ puts ' cognito_authenticatable, ttl_attribute),'
285
351
  puts ' so a hand-added definition is invisible to the generator and gets overwritten.'
286
352
  end
287
353
 
354
+ def warn_dropped_comments(dest, comments)
355
+ return if comments.empty?
356
+
357
+ puts " ⚠ #{dest}: removed #{comments.size} hand-written comment line(s) " \
358
+ '(the file is regenerated — put notes on the model instead)'
359
+ end
360
+
288
361
  def render_dynamodb(models)
289
362
  blocks = models.map { |m| render_table(m) }
290
363
  "# Auto-generated by Belt from model definitions in lambda/models/*.rb\n" \
291
364
  "#\n" \
292
365
  "# Do NOT edit manually. `belt setup tables` overwrites this whole file from\n" \
293
366
  "# 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"
367
+ "# declare) will be DROPPED on the next regeneration, as will ttl blocks and\n" \
368
+ "# comments. Define indexes on the model via indexes(), belongs_to, or\n" \
369
+ "# cognito_authenticatable, and TTL via ttl_attribute, instead.\n\n#{blocks.join("\n\n")}\n"
296
370
  end
297
371
 
298
372
  def render_table(model)
@@ -360,6 +434,14 @@ module Belt
360
434
  lines << ' }'
361
435
  end
362
436
 
437
+ if model[:ttl]
438
+ lines << ''
439
+ lines << ' ttl {'
440
+ lines << " attribute_name = \"#{model[:ttl]}\""
441
+ lines << ' enabled = true'
442
+ lines << ' }'
443
+ end
444
+
363
445
  lines << ''
364
446
  lines << ' point_in_time_recovery {'
365
447
  lines << ' enabled = var.enable_pitr'
@@ -144,6 +144,26 @@ ActiveItem::Transaction.write do |tx|
144
144
  end
145
145
  ```
146
146
 
147
+ ## Time To Live (TTL)
148
+
149
+ Let DynamoDB expire rows by declaring the TTL attribute on the model:
150
+
151
+ ```ruby
152
+ class Session < ApplicationRecord
153
+ ttl_attribute :expires_at # stored as expiresAt; must hold epoch seconds
154
+ end
155
+ ```
156
+
157
+ `belt setup tables` emits the matching `ttl { attribute_name = "expiresAt" }` block in
158
+ `dynamodb.tf`. A symbol is camelCased the way ActiveItem writes it; pass a string
159
+ (`ttl_attribute 'expires_at_epoch'`) to use the DynamoDB attribute name as-is.
160
+
161
+ Don't add `ttl` (or any other block) to `dynamodb.tf` by hand. The generator overwrites
162
+ the whole file from your models. If a regeneration would drop a hand-added table, GSI,
163
+ `ttl` or other table setting, `belt setup tables` asks before overwriting, and a
164
+ generator's auto-sync skips the file. Comments in the file are lost too (Belt prints a
165
+ warning), so put notes on the model.
166
+
147
167
  ## Generating Models
148
168
 
149
169
  ```bash
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'activeitem'
4
+ require_relative '../inflector'
5
+
6
+ module Belt
7
+ module Models
8
+ # Declares the DynamoDB Time To Live attribute for a model's table:
9
+ #
10
+ # class Session < ApplicationRecord
11
+ # ttl_attribute :expires_at # → ttl { attribute_name = "expiresAt" }
12
+ # end
13
+ #
14
+ # DynamoDB does the expiring, so at runtime this only records the name. The point
15
+ # is that `belt setup tables` reads it and emits the `ttl { }` block — a block
16
+ # added straight into dynamodb.tf would be dropped by the next regeneration.
17
+ #
18
+ # A symbol is camelCased the way ActiveItem stores it (`:expires_at` → `expiresAt`).
19
+ # Pass a string to use the DynamoDB attribute name verbatim (e.g. when the model
20
+ # remaps it with dynamo_attribute_map).
21
+ module TtlMacro
22
+ def ttl_attribute(name = nil)
23
+ return @ttl_attribute if name.nil?
24
+
25
+ @ttl_attribute = name.is_a?(Symbol) ? Belt::Inflector.camelize_lower(name.to_s) : name.to_s
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ ActiveItem::Base.extend(Belt::Models::TtlMacro)
data/lib/belt/version.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Belt
4
- VERSION = '0.6.7'
4
+ VERSION = '0.6.8'
5
5
 
6
6
  # Ruby version new apps run on in Lambda. Rendered into the conveyor-belt
7
7
  # provider block (`ruby_version`) by `belt new` / `belt generate environment`.
data/lib/belt.rb CHANGED
@@ -55,5 +55,6 @@ module Belt
55
55
  end
56
56
 
57
57
  require_relative 'belt/authentication'
58
+ require_relative 'belt/models/ttl_macro'
58
59
  require_relative 'belt_controller/base'
59
60
  require_relative 'belt/controllers/welcome_controller'
@@ -57,6 +57,11 @@ belt apply <env>
57
57
 
58
58
  This creates: model, controller (with all CRUD actions), route entry, and DynamoDB schema.
59
59
 
60
+ `infrastructure/modules/app/dynamodb.tf` is generated. Declare everything on the model:
61
+ GSIs via `indexes(...)` / `belongs_to`, TTL via `ttl_attribute :expires_at`. Anything
62
+ you add to the `.tf` by hand (a `ttl` block, a comment) gets dropped the next time it's
63
+ regenerated.
64
+
60
65
  ### Add a model only (no API endpoint)
61
66
 
62
67
  ```bash
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.6.7
4
+ version: 0.6.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stowzilla
@@ -186,6 +186,7 @@ files:
186
186
  - lib/belt/inflector.rb
187
187
  - lib/belt/lambda_handler.rb
188
188
  - lib/belt/migration.rb
189
+ - lib/belt/models/ttl_macro.rb
189
190
  - lib/belt/observability.rb
190
191
  - lib/belt/parameters.rb
191
192
  - lib/belt/rendering.rb