dynamoid-cdk-schema 0.1.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e17f72b5bacad48e8d0df3aac32d6d27332a6a14c8b3f1ed39d2ef3d1e5ca5de
4
+ data.tar.gz: 92cdbec04b2b58ab80dba06bc809eb3ddffeec03a2107de46cad9f5a003eb37a
5
+ SHA512:
6
+ metadata.gz: d54877e0eb240cfcb1727d179c7eff84382db1fd6809a3495826f84ecc236e2bd7ce6e71e682c9c18a5803dc68283b8695294fe732f60cb25a657d7a1fae76c4
7
+ data.tar.gz: da0ac57c6fdedf438eec444575ae2cf2e88de1692d4fc15bc6fb69b5b3b71acd95ce222e1beec9d4d960560b307e5c7e7dab0c7d4fb39406773e7b441bb91927
data/CHANGELOG.md ADDED
@@ -0,0 +1,10 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2026-07-27
4
+
5
+ - Initial release.
6
+ - `Dynamoid::CDK::Schema.describe(model)` — reads a Dynamoid model's schema (partition/sort keys, global and local
7
+ secondary indexes, attribute types) into a plain descriptor. Dynamoid only, no AWS CDK required.
8
+ - `Dynamoid::CDK::Schema.table(scope, id, model, **table_props)` — builds the matching CDK `TableV2`; extra keyword
9
+ args pass through. `aws-cdk-lib` is referenced lazily (the caller's dependency).
10
+ - Supports `ALL` / `KEYS_ONLY` / `INCLUDE` (with `non_key_attributes`) projections and binary key attributes.
@@ -0,0 +1,10 @@
1
+ # Code of Conduct
2
+
3
+ "dynamoid-cdk-schema" follows [The Ruby Community Conduct Guideline](https://www.ruby-lang.org/en/conduct) in all "collaborative space", which is defined as community communications channels (such as mailing lists, submitted patches, commit comments, etc.):
4
+
5
+ * Participants will be tolerant of opposing views.
6
+ * Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks.
7
+ * When interpreting the words and actions of others, participants should always assume good intentions.
8
+ * Behaviour which can be reasonably considered harassment will not be tolerated.
9
+
10
+ If you have any concerns about behaviour within this project, please contact us at ["omar@omarqureshi.net"](mailto:"omar@omarqureshi.net").
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Omar Qureshi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,89 @@
1
+ # dynamoid-cdk-schema
2
+
3
+ Generate [AWS CDK](https://docs.aws.amazon.com/cdk/) DynamoDB tables straight from your
4
+ [Dynamoid](https://github.com/Dynamoid/dynamoid) models.
5
+
6
+ Your Dynamoid models already declare their keys and indexes. Restating that schema by hand in your CDK stack means two
7
+ sources of truth that drift the moment you add a GSI. This gem reads a model's schema — partition and sort keys, global
8
+ and local secondary indexes, attribute types — and builds the matching `TableV2`, so the model is the single source of
9
+ truth for both the application and its infrastructure.
10
+
11
+ It works because the AWS CDK is a first-class Ruby library your infrastructure code can `require` and reflect over — the
12
+ same reason it can read your Dynamoid models. There is no separate template to keep in sync.
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ bundle add dynamoid-cdk-schema
18
+ ```
19
+
20
+ `aws-cdk-lib` is intentionally **not** a dependency — it's your CDK app's, and it's referenced lazily, only when you
21
+ build a table. So the introspection half (`.describe`) works anywhere Dynamoid is loaded.
22
+
23
+ ## Usage
24
+
25
+ Inside a CDK stack (where `aws-cdk-lib` and your Dynamoid models are already loaded):
26
+
27
+ ```ruby
28
+ class MyStack < AWSCDK::Stack
29
+ def initialize(scope, id, **kwargs)
30
+ super
31
+
32
+ # Build a TableV2 from the model. Extra keyword args pass straight through to
33
+ # TableV2, so the model owns keys + indexes and you own table-level config.
34
+ posts = Dynamoid::CDK::Schema.table(self, "Posts", Post,
35
+ removal_policy: AWSCDK::RemovalPolicy::RETAIN)
36
+
37
+ # `posts` is an ordinary AWSCDK::DynamoDB::TableV2 — grant it, reference it, etc.
38
+ posts.grant_read_write_data(my_function)
39
+ end
40
+ end
41
+ ```
42
+
43
+ That's it. If `Post` declares a `global_secondary_index` or `range` key, the table gets it, with the right attribute
44
+ types and projection.
45
+
46
+ ### Just the schema, as data
47
+
48
+ `.describe` needs only Dynamoid (no CDK) and returns a plain, immutable descriptor — handy for tests, tooling, or
49
+ feeding another target:
50
+
51
+ ```ruby
52
+ Dynamoid::CDK::Schema.describe(Post)
53
+ # => #<data Dynamoid::CDK::Schema::Descriptor
54
+ # partition_key=#<data Attribute name="id", type=:string>,
55
+ # sort_key=nil,
56
+ # global_secondary_indexes=[#<data Index name="by_recency", ...>],
57
+ # local_secondary_indexes=[]>
58
+ ```
59
+
60
+ ### What it reads
61
+
62
+ - Partition and sort keys, with DynamoDB attribute types (`:string` / `:number` / `:binary`) resolved the way Dynamoid
63
+ resolves them.
64
+ - Every **global secondary index**: keys, and the projection type — `ALL`, `KEYS_ONLY`, or `INCLUDE` (with its
65
+ `non_key_attributes`).
66
+ - **Local secondary indexes**.
67
+
68
+ Table-level configuration Dynamoid doesn't model (billing mode, TTL, point-in-time recovery, streams, removal policy)
69
+ is yours to pass through as keyword arguments — this gem is about the *schema*, keys and indexes.
70
+
71
+ ## Loading your models at synth time
72
+
73
+ `cdk synth` has to be able to load the model classes you pass in, so `require` them from your CDK app. If a model reads
74
+ application config at load time (e.g. its table name from Rails config), make that available in the synth process — the
75
+ CDK never uses the physical table name (it assigns one), so a placeholder is enough.
76
+
77
+ ## Development
78
+
79
+ After checking out the repo, run `bin/setup` to install dependencies, then `bundle exec rspec` to run the tests and
80
+ `bundle exec rubocop` to lint. `bin/console` gives an interactive prompt.
81
+
82
+ ## Contributing
83
+
84
+ Bug reports and pull requests are welcome on GitHub at
85
+ <https://github.com/omarqureshi/dynamoid-cdk-schema>.
86
+
87
+ ## License
88
+
89
+ Available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dynamoid
4
+ module CDK
5
+ module Schema
6
+ # A DynamoDB attribute used as a key: its name and DynamoDB type
7
+ # (+:string+, +:number+ or +:binary+).
8
+ Attribute = Data.define(:name, :type)
9
+
10
+ # A secondary index. For a GSI +partition_key+ is set; for an LSI it is nil
11
+ # (an LSI shares the table's partition key). +projection_type+ is +:all+,
12
+ # +:keys_only+ or +:include+; +non_key_attributes+ is the projected list
13
+ # when +:include+, otherwise nil.
14
+ Index = Data.define(:name, :partition_key, :sort_key, :projection_type, :non_key_attributes)
15
+
16
+ # A model's table shape as plain data — readable and buildable with no AWS
17
+ # CDK present. +sort_key+ is nil for a hash-only table.
18
+ Descriptor = Data.define(:partition_key, :sort_key, :global_secondary_indexes, :local_secondary_indexes)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dynamoid"
4
+
5
+ module Dynamoid
6
+ module CDK
7
+ module Schema
8
+ # Reads a Dynamoid model class into a {Descriptor}: partition and sort keys,
9
+ # GSIs and LSIs, with DynamoDB attribute types resolved exactly the way
10
+ # Dynamoid resolves them for its own tables. Pure metadata — no DynamoDB
11
+ # connection and no AWS CDK.
12
+ module Introspector
13
+ module_function
14
+
15
+ def call(model)
16
+ Descriptor.new(
17
+ partition_key: attribute(model, model.hash_key),
18
+ sort_key: (attribute(model, model.range_key) if model.range_key),
19
+ global_secondary_indexes: model.global_secondary_indexes.values.map { |i| gsi(model, i) },
20
+ local_secondary_indexes: model.local_secondary_indexes.values.map { |i| lsi(model, i) }
21
+ )
22
+ end
23
+
24
+ def attribute(model, name)
25
+ field = model.attributes[name.to_sym]
26
+ raise Error, "#{model}: no field #{name.inspect} declared for a key attribute" if field.nil?
27
+
28
+ Attribute.new(name: name.to_s, type: dynamodb_type(field))
29
+ end
30
+
31
+ # Resolve a field's DynamoDB key type the way Dynamoid does. Its mapping
32
+ # doesn't emit :binary, but DynamoDB permits binary keys, so fall back to
33
+ # the field type on UnsupportedKeyType — mirroring Dynamoid's own index
34
+ # validation.
35
+ def dynamodb_type(field)
36
+ Dynamoid::PrimaryKeyTypeMapping.dynamodb_type(field[:type], field)
37
+ rescue Dynamoid::Errors::UnsupportedKeyType
38
+ field[:type]
39
+ end
40
+
41
+ def gsi(model, index)
42
+ Index.new(
43
+ name: index.name,
44
+ partition_key: attribute(model, index.hash_key),
45
+ sort_key: (attribute(model, index.range_key) if index.range_key),
46
+ projection_type: index.projection_type,
47
+ non_key_attributes: non_key_attributes(index)
48
+ )
49
+ end
50
+
51
+ def lsi(model, index)
52
+ Index.new(
53
+ name: index.name,
54
+ partition_key: nil, # an LSI shares the table's partition key
55
+ sort_key: attribute(model, index.range_key),
56
+ projection_type: index.projection_type,
57
+ non_key_attributes: non_key_attributes(index)
58
+ )
59
+ end
60
+
61
+ # DynamoDB wants the projected attribute list only for INCLUDE projections.
62
+ def non_key_attributes(index)
63
+ return nil unless index.projection_type == :include
64
+
65
+ Array(index.projected_attributes).map(&:to_s)
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dynamoid
4
+ module CDK
5
+ module Schema
6
+ # Turns a {Descriptor} into an AWS CDK +TableV2+. AWS CDK is the caller's
7
+ # dependency (the Ruby CDK, from its preview feed), so it is referenced
8
+ # lazily at build time — the {Introspector} half of the gem works without
9
+ # it, and a clear error is raised if +.table+ is called without it loaded.
10
+ module TableBuilder
11
+ module_function
12
+
13
+ def build(scope, id, descriptor, **table_props)
14
+ props = table_props.merge(key_props(descriptor), index_props(descriptor))
15
+ dynamodb::TableV2.new(scope, id, props)
16
+ end
17
+
18
+ def key_props(descriptor)
19
+ props = { partition_key: cdk_attribute(descriptor.partition_key) }
20
+ props[:sort_key] = cdk_attribute(descriptor.sort_key) if descriptor.sort_key
21
+ props
22
+ end
23
+
24
+ def index_props(descriptor)
25
+ props = {}
26
+ gsis = descriptor.global_secondary_indexes
27
+ props[:global_secondary_indexes] = gsis.map { |i| cdk_gsi(i) } if gsis.any?
28
+ lsis = descriptor.local_secondary_indexes
29
+ props[:local_secondary_indexes] = lsis.map { |i| cdk_lsi(i) } if lsis.any?
30
+ props
31
+ end
32
+
33
+ def cdk_attribute(attr)
34
+ { name: attr.name, type: cdk_attribute_type(attr.type) }
35
+ end
36
+
37
+ def cdk_gsi(index)
38
+ props = {
39
+ index_name: index.name,
40
+ partition_key: cdk_attribute(index.partition_key),
41
+ projection_type: cdk_projection_type(index.projection_type)
42
+ }
43
+ props[:sort_key] = cdk_attribute(index.sort_key) if index.sort_key
44
+ props[:non_key_attributes] = index.non_key_attributes if index.non_key_attributes
45
+ props
46
+ end
47
+
48
+ def cdk_lsi(index)
49
+ props = {
50
+ index_name: index.name,
51
+ sort_key: cdk_attribute(index.sort_key),
52
+ projection_type: cdk_projection_type(index.projection_type)
53
+ }
54
+ props[:non_key_attributes] = index.non_key_attributes if index.non_key_attributes
55
+ props
56
+ end
57
+
58
+ def cdk_attribute_type(type)
59
+ case type
60
+ when :string then dynamodb::AttributeType::STRING
61
+ when :number then dynamodb::AttributeType::NUMBER
62
+ when :binary then dynamodb::AttributeType::BINARY
63
+ else raise Error, "unsupported key attribute type #{type.inspect}"
64
+ end
65
+ end
66
+
67
+ def cdk_projection_type(type)
68
+ case type
69
+ when :all then dynamodb::ProjectionType::ALL
70
+ when :keys_only then dynamodb::ProjectionType::KEYS_ONLY
71
+ when :include then dynamodb::ProjectionType::INCLUDE
72
+ else raise Error, "unsupported projection type #{type.inspect}"
73
+ end
74
+ end
75
+
76
+ def dynamodb
77
+ return AWSCDK::DynamoDB if defined?(AWSCDK::DynamoDB)
78
+
79
+ raise Error,
80
+ "aws-cdk-lib must be loaded to build tables — add it to your CDK app and `require \"aws-cdk-lib\"`"
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dynamoid
4
+ module CDK
5
+ module Schema
6
+ VERSION = "0.1.0"
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "schema/version"
4
+
5
+ module Dynamoid
6
+ module CDK
7
+ module Schema
8
+ class Error < StandardError; end
9
+ end
10
+ end
11
+ end
12
+
13
+ require_relative "schema/descriptor"
14
+ require_relative "schema/introspector"
15
+ require_relative "schema/table_builder"
16
+
17
+ module Dynamoid
18
+ module CDK
19
+ # Generate AWS CDK DynamoDB tables straight from your Dynamoid models, so the
20
+ # models are the single source of truth for keys and indexes and the two
21
+ # can't drift.
22
+ module Schema
23
+ module_function
24
+
25
+ # The model's table shape as plain data (a {Descriptor}). Needs only
26
+ # Dynamoid — handy for inspection, tests, or feeding another tool.
27
+ def describe(model)
28
+ Introspector.call(model)
29
+ end
30
+
31
+ # Build and return a CDK +TableV2+ for +model+ in +scope+ under +id+. Any
32
+ # extra keyword arguments pass straight through to +TableV2+ (e.g.
33
+ # +removal_policy:+, +billing:+, +point_in_time_recovery:+), so the model
34
+ # owns keys and indexes while the caller owns table-level configuration.
35
+ # Requires aws-cdk-lib to be loaded.
36
+ #
37
+ # Dynamoid::CDK::Schema.table(self, "Posts", Post, removal_policy: ...)
38
+ def table(scope, id, model, **table_props)
39
+ TableBuilder.build(scope, id, describe(model), **table_props)
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,8 @@
1
+ module Dynamoid
2
+ module CDK
3
+ module Schema
4
+ VERSION: String
5
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
6
+ end
7
+ end
8
+ end
metadata ADDED
@@ -0,0 +1,77 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dynamoid-cdk-schema
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Omar Qureshi
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: dynamoid
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '3.9'
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '4'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: '3.9'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '4'
32
+ description: Reads a Dynamoid model's schema — partition/sort keys, global and local
33
+ secondary indexes, attribute types — and builds the matching AWS CDK TableV2, so
34
+ your models are the single source of truth for both the app and its infrastructure.
35
+ email:
36
+ - omar@omarqureshi.net
37
+ executables: []
38
+ extensions: []
39
+ extra_rdoc_files: []
40
+ files:
41
+ - CHANGELOG.md
42
+ - CODE_OF_CONDUCT.md
43
+ - LICENSE.txt
44
+ - README.md
45
+ - Rakefile
46
+ - lib/dynamoid/cdk/schema.rb
47
+ - lib/dynamoid/cdk/schema/descriptor.rb
48
+ - lib/dynamoid/cdk/schema/introspector.rb
49
+ - lib/dynamoid/cdk/schema/table_builder.rb
50
+ - lib/dynamoid/cdk/schema/version.rb
51
+ - sig/dynamoid/cdk/schema.rbs
52
+ homepage: https://github.com/omarqureshi/dynamoid-cdk-schema
53
+ licenses:
54
+ - MIT
55
+ metadata:
56
+ allowed_push_host: https://rubygems.org
57
+ homepage_uri: https://github.com/omarqureshi/dynamoid-cdk-schema
58
+ source_code_uri: https://github.com/omarqureshi/dynamoid-cdk-schema
59
+ changelog_uri: https://github.com/omarqureshi/dynamoid-cdk-schema/blob/main/CHANGELOG.md
60
+ rdoc_options: []
61
+ require_paths:
62
+ - lib
63
+ required_ruby_version: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: 3.2.0
68
+ required_rubygems_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: '0'
73
+ requirements: []
74
+ rubygems_version: 4.0.10
75
+ specification_version: 4
76
+ summary: Generate AWS CDK DynamoDB tables from your Dynamoid models.
77
+ test_files: []