plutonium 0.40.0 → 0.41.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ba93910895cf945960fdec47e9ba7026df83718d63e947a68ed4eb78ccd2ff4a
4
- data.tar.gz: fbff28e4bfefc0fb86a85ef6e8c5d8272b2669a0048e7e9465c882bcfa1bb07c
3
+ metadata.gz: 23a6720a12194c10b70fc95f5c9b4f2b8aca0906191ed4488238e13048004de4
4
+ data.tar.gz: bc178cbd666304c9e170cac636a10ad4c066d104d2397428dc038640ab09091a
5
5
  SHA512:
6
- metadata.gz: 3c6bf1a8172e6898415f7643e9170e9ca8541980b49346e05a74264b8d19deb94ff3f8b42f5cd1c7a256aa3bb35f112ee542d687b20dfc361a770da5e77b6414
7
- data.tar.gz: a70bdce9041d0e729a65f225c4211a97be8c4c97e378b9138079edcd3282a8a33cae73e2f230b9054f2041ba8dffe6035f32aab4e3ae87fb27ba7b8646cb781e
6
+ metadata.gz: ba109a23c3f75f43a453a7a195a15e3b45e334f4751c2747b43c5b82cac6832b696f48bfe4d480015c4b9171e4f47de21f0981e9e40697f069432d592d1d0643
7
+ data.tar.gz: 3bbbd2a4d1a69e92c87475b41c452a478f1ba671b413e6e01d358cbe813fc59b91e5c29b9a22e74549575ac5ed6c38d3f8225f02890a7ff376db1a78ede6c72c
@@ -83,18 +83,43 @@ Format: `name:type:index_type`
83
83
  | `'ends_at:datetime?'` | Nullable datetime |
84
84
  | `alarm_time:time` | Required time |
85
85
  | `'reminder_time:time?'` | Nullable time |
86
+ | `metadata:json` | JSON field |
87
+ | `settings:jsonb` | JSONB (PostgreSQL) |
88
+ | `external_id:uuid` | UUID field |
86
89
 
87
- ### Decimal with Precision
90
+ ### PostgreSQL-Specific Types
88
91
 
89
- The `{precision,scale}` syntax **only works for decimal types**:
92
+ These types work in both PostgreSQL and SQLite (automatically mapped):
93
+
94
+ | Type | PostgreSQL | SQLite |
95
+ |------|------------|--------|
96
+ | `jsonb` | `jsonb` | `json` |
97
+ | `hstore` | `hstore` | `json` |
98
+ | `uuid` | `uuid` | `string` |
99
+ | `inet` | `inet` | `string` |
100
+ | `cidr` | `cidr` | `string` |
101
+ | `macaddr` | `macaddr` | `string` |
102
+ | `ltree` | `ltree` | `string` |
103
+
104
+ ### Default Values
105
+
106
+ Use `{default:value}` syntax for default values:
90
107
 
91
108
  ```bash
92
- 'latitude:decimal{11,8}' # precision: 11, scale: 8
93
- 'amount:decimal{10,2}' # precision: 10, scale: 2
94
- 'latitude:decimal?{11,8}' # nullable with precision
109
+ 'status:string{default:draft}' # String default
110
+ 'active:boolean{default:true}' # Boolean default (true/false/yes/1)
111
+ 'priority:integer{default:0}' # Integer default
112
+ 'rating:float{default:4.5}' # Float default
113
+ 'status:string?{default:pending}' # Nullable with default
95
114
  ```
96
115
 
97
- **Note**: For default values on other types (boolean, integer, etc.), edit the migration manually.
116
+ ### Decimal with Precision and Default
117
+
118
+ ```bash
119
+ 'amount:decimal{10,2}' # precision: 10, scale: 2
120
+ 'price:decimal{10,2,default:0}' # with default value
121
+ 'balance:decimal?{15,2,default:0}' # nullable with precision and default
122
+ ```
98
123
 
99
124
  ### References/Associations
100
125
 
@@ -103,6 +128,8 @@ company:belongs_to # Required foreign key
103
128
  'parent:belongs_to?' # Nullable (null: true + optional: true)
104
129
  user:references # Same as belongs_to
105
130
  blogging/post:belongs_to # Cross-package reference
131
+ 'author:belongs_to{class_name:User}' # Custom class_name (author_id -> User)
132
+ 'reviewer:belongs_to?{class_name:User}' # Nullable with class_name
106
133
  ```
107
134
 
108
135
  Nullable references generate:
@@ -179,10 +206,12 @@ t.belongs_to :parent, null: false, foreign_key: {on_delete: :cascade}
179
206
 
180
207
  ### Default Values
181
208
 
209
+ Default values can be set directly in the generator using `{default:value}` syntax (see Field Type Syntax above). For more complex defaults or expressions, edit the migration:
210
+
182
211
  ```ruby
183
212
  t.boolean :is_active, default: true
184
213
  t.integer :status, default: 0
185
- t.integer :count, null: true, default: 0
214
+ t.datetime :published_at, default: -> { "CURRENT_TIMESTAMP" }
186
215
  ```
187
216
 
188
217
  ## Examples
data/CHANGELOG.md CHANGED
@@ -1,3 +1,27 @@
1
+ ## [0.41.0] - 2026-02-09
2
+
3
+ ### 🚀 Features
4
+
5
+ - *(generators)* Add default value support and improve SQLite compatibility
6
+ - *(generators)* Add class_name option for belongs_to fields
7
+ - *(generators)* Add --policy and --definition flags to conn generator
8
+
9
+ ### 🐛 Bug Fixes
10
+
11
+ - *(generators)* Fix has_many association injection pattern
12
+ - *(generators)* Add missing scripts to package.json in assets generator
13
+ - *(generators)* Remove primary account support for rodauth
14
+ - *(generators)* Use top-level Gem::Version to avoid namespace collision
15
+ - *(ui)* Check resource registration before generating association add URL
16
+
17
+ ### 📚 Documentation
18
+
19
+ - Add class_name option and associations section to generator docs
20
+
21
+ ### 🧪 Testing
22
+
23
+ - *(generators)* Add tests for named rodauth account configuration
24
+ - *(sqlite)* Move type alias tests to core and test actual migrations
1
25
  ## [0.40.0] - 2026-02-04
2
26
 
3
27
  ### 🚀 Features
@@ -1,15 +1,58 @@
1
- # Alias json to jsonb in SQLite migrations
1
+ # Alias PostgreSQL-specific types for SQLite compatibility in migrations
2
+ #
3
+ # This allows using PostgreSQL-specific column types (jsonb, uuid, etc.) in migrations
4
+ # while developing with SQLite. The types are mapped to SQLite equivalents.
5
+
6
+ # Type mappings: PostgreSQL type -> SQLite equivalent
7
+ PLUTONIUM_SQLITE_TYPE_ALIASES = {
8
+ jsonb: :json,
9
+ hstore: :json,
10
+ uuid: :string,
11
+ inet: :string,
12
+ cidr: :string,
13
+ macaddr: :string,
14
+ ltree: :string
15
+ }.freeze
2
16
 
3
17
  ActiveSupport.on_load(:active_record) do
4
- if defined?(ActiveRecord::ConnectionAdapters::SQLite3::TableDefinition)
5
- ActiveRecord::ConnectionAdapters::SQLite3::TableDefinition.class_eval do
6
- def jsonb(*args, **options)
7
- json(*args, **options)
18
+ next unless defined?(ActiveRecord::ConnectionAdapters::SQLite3Adapter)
19
+
20
+ # Add methods to TableDefinition for create_table blocks
21
+ ActiveRecord::ConnectionAdapters::SQLite3::TableDefinition.class_eval do
22
+ PLUTONIUM_SQLITE_TYPE_ALIASES.each do |pg_type, sqlite_type|
23
+ define_method(pg_type) do |*args, **options|
24
+ send(sqlite_type, *args, **options)
8
25
  end
26
+ end
27
+ end
9
28
 
10
- def uuid(*args, **options)
11
- string(*args, **options)
29
+ # Override valid_type? to accept PostgreSQL types (instance method)
30
+ ActiveRecord::ConnectionAdapters::SQLite3Adapter.class_eval do
31
+ alias_method :original_valid_type?, :valid_type?
32
+
33
+ def valid_type?(type)
34
+ PLUTONIUM_SQLITE_TYPE_ALIASES.key?(type&.to_sym) || original_valid_type?(type)
35
+ end
36
+ end
37
+
38
+ # Override valid_type? class method for Rails generators (Rails 8.1+)
39
+ if ActiveRecord::ConnectionAdapters::SQLite3Adapter.respond_to?(:valid_type?)
40
+ ActiveRecord::ConnectionAdapters::SQLite3Adapter.singleton_class.class_eval do
41
+ alias_method :original_valid_type?, :valid_type?
42
+
43
+ def valid_type?(type)
44
+ PLUTONIUM_SQLITE_TYPE_ALIASES.key?(type&.to_sym) || original_valid_type?(type)
12
45
  end
13
46
  end
14
47
  end
48
+
49
+ # Override type_to_sql to map PostgreSQL types to SQLite equivalents
50
+ ActiveRecord::ConnectionAdapters::SQLite3Adapter.class_eval do
51
+ alias_method :original_type_to_sql, :type_to_sql
52
+
53
+ def type_to_sql(type, **)
54
+ mapped_type = PLUTONIUM_SQLITE_TYPE_ALIASES[type&.to_sym] || type
55
+ original_type_to_sql(mapped_type, **)
56
+ end
57
+ end
15
58
  end
@@ -44,6 +44,8 @@ Format: `name:type:index_type`
44
44
  | `datetime` | `published_at:datetime` | Date and time |
45
45
  | `time` | `starts_at:time` | Time only |
46
46
  | `json` | `metadata:json` | JSON data |
47
+ | `jsonb` | `settings:jsonb` | JSONB (PostgreSQL) |
48
+ | `uuid` | `external_id:uuid` | UUID field |
47
49
 
48
50
  ### Nullable Fields
49
51
 
@@ -65,6 +67,18 @@ Use `{precision,scale}` syntax for decimal fields:
65
67
  'amount:decimal?{15,2}' # nullable with precision
66
68
  ```
67
69
 
70
+ ### Default Values
71
+
72
+ Use `{default:value}` syntax to set default values:
73
+
74
+ ```bash
75
+ 'status:string{default:draft}' # String default
76
+ 'active:boolean{default:true}' # Boolean default
77
+ 'priority:integer{default:0}' # Integer default
78
+ 'price:decimal{10,2,default:0}' # Decimal with precision and default
79
+ 'category:string?{default:general}' # Nullable with default
80
+ ```
81
+
68
82
  ### Associations
69
83
 
70
84
  ```bash
@@ -75,6 +89,10 @@ company:references # Same as belongs_to
75
89
  # Nullable belongs_to
76
90
  'parent:belongs_to?' # Creates: null: true, optional: true
77
91
 
92
+ # Custom class_name (column author_id -> User model)
93
+ 'author:belongs_to{class_name:User}'
94
+ 'reviewer:belongs_to?{class_name:User}' # Nullable
95
+
78
96
  # Cross-package reference
79
97
  blogging/post:belongs_to
80
98
  ```
@@ -359,9 +377,11 @@ t.belongs_to :user, null: false, foreign_key: {on_delete: :cascade}
359
377
 
360
378
  ### Default Values
361
379
 
380
+ Default values can be set directly in the generator using `{default:value}` syntax. For expressions or complex defaults, edit the migration:
381
+
362
382
  ```ruby
363
383
  t.boolean :is_active, default: true
364
- t.integer :status, default: 0
384
+ t.datetime :published_at, default: -> { "CURRENT_TIMESTAMP" }
365
385
  ```
366
386
 
367
387
  ## Removing Resources
@@ -39,6 +39,9 @@ rails generate pu:res:scaffold Post
39
39
  # With associations
40
40
  rails generate pu:res:scaffold Comment body:text user:belongs_to post:belongs_to
41
41
 
42
+ # With custom class_name (author -> User)
43
+ rails generate pu:res:scaffold Post 'author:belongs_to{class_name:User}'
44
+
42
45
  # Skip model generation for existing models
43
46
  rails generate pu:res:scaffold Post title:string --no-model
44
47
  ```
@@ -57,16 +60,76 @@ rails generate pu:res:scaffold Post title:string --no-model
57
60
  | `datetime` | `published_at:datetime` | `datetime` |
58
61
  | `time` | `starts_at:time` | `time` |
59
62
  | `json` | `metadata:json` | `json` |
63
+ | `jsonb` | `settings:jsonb` | `jsonb` (PostgreSQL) / `json` (SQLite) |
64
+ | `uuid` | `external_id:uuid` | `uuid` (PostgreSQL) / `string` (SQLite) |
60
65
  | `belongs_to` | `user:belongs_to` | `references` |
61
66
  | `references` | `user:references` | `references` |
62
67
  | `rich_text` | `content:rich_text` | Action Text |
63
68
 
69
+ #### PostgreSQL-Specific Types
70
+
71
+ These types work in both PostgreSQL and SQLite (mapped to compatible types):
72
+
73
+ | Type | PostgreSQL | SQLite Equivalent |
74
+ |------|------------|-------------------|
75
+ | `jsonb` | `jsonb` | `json` |
76
+ | `hstore` | `hstore` | `json` |
77
+ | `uuid` | `uuid` | `string` |
78
+ | `inet` | `inet` | `string` |
79
+ | `cidr` | `cidr` | `string` |
80
+ | `macaddr` | `macaddr` | `string` |
81
+ | `ltree` | `ltree` | `string` |
82
+
64
83
  #### Nullable Fields
65
84
 
66
85
  Append `?` to make a field nullable:
67
86
 
68
87
  ```bash
69
- rails generate pu:res:scaffold Post title:string description:text?
88
+ rails generate pu:res:scaffold Post title:string 'description:text?'
89
+ ```
90
+
91
+ #### Default Values
92
+
93
+ Use `{default:value}` syntax to set default values:
94
+
95
+ ```bash
96
+ # String with default
97
+ rails generate pu:res:scaffold Post 'status:string{default:draft}'
98
+
99
+ # Boolean with default
100
+ rails generate pu:res:scaffold Post 'active:boolean{default:true}'
101
+
102
+ # Integer with default
103
+ rails generate pu:res:scaffold Post 'priority:integer{default:0}'
104
+
105
+ # Decimal with precision and default
106
+ rails generate pu:res:scaffold Product 'price:decimal{10,2,default:0}'
107
+
108
+ # Nullable with default
109
+ rails generate pu:res:scaffold Post 'category:string?{default:general}'
110
+ ```
111
+
112
+ ::: tip Shell Quoting
113
+ Always quote fields containing `?` or `{}` to prevent shell expansion.
114
+ :::
115
+
116
+ #### Associations
117
+
118
+ ```bash
119
+ # Required belongs_to
120
+ rails generate pu:res:scaffold Comment user:belongs_to post:belongs_to
121
+
122
+ # Nullable belongs_to
123
+ rails generate pu:res:scaffold Post 'parent:belongs_to?'
124
+
125
+ # Custom class_name (author_id column -> User model)
126
+ rails generate pu:res:scaffold Post 'author:belongs_to{class_name:User}'
127
+
128
+ # Nullable with custom class_name
129
+ rails generate pu:res:scaffold Post 'reviewer:belongs_to?{class_name:User}'
130
+
131
+ # Cross-package reference
132
+ rails generate pu:res:scaffold Comment blogging/post:belongs_to
70
133
  ```
71
134
 
72
135
  #### Money Fields (has_cents)
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- plutonium (0.39.2)
4
+ plutonium (0.40.0)
5
5
  action_policy (~> 0.7.0)
6
6
  listen (~> 3.8)
7
7
  pagy (~> 9.0)
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- plutonium (0.39.2)
4
+ plutonium (0.40.0)
5
5
  action_policy (~> 0.7.0)
6
6
  listen (~> 3.8)
7
7
  pagy (~> 9.0)
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- plutonium (0.39.2)
4
+ plutonium (0.40.0)
5
5
  action_policy (~> 0.7.0)
6
6
  listen (~> 3.8)
7
7
  pagy (~> 9.0)
@@ -58,9 +58,14 @@ module Pu
58
58
  end
59
59
 
60
60
  def replace_build_script
61
- gsub_file "package.json",
62
- /"build:css":.*/,
63
- '"build:css": "postcss ./app/assets/stylesheets/application.tailwind.css -o ./app/assets/builds/application.css"'
61
+ package_json = File.read("package.json")
62
+ package = JSON.parse(package_json)
63
+
64
+ package["scripts"] ||= {}
65
+ package["scripts"]["build"] = "esbuild app/javascript/*.* --bundle --sourcemap --format=esm --outdir=app/assets/builds --public-path=/assets"
66
+ package["scripts"]["build:css"] = "postcss ./app/assets/stylesheets/application.tailwind.css -o ./app/assets/builds/application.css"
67
+
68
+ File.write("package.json", JSON.pretty_generate(package) + "\n")
64
69
  end
65
70
 
66
71
  def import_styles
@@ -127,8 +127,8 @@ module Pu
127
127
 
128
128
  def add_entity_association
129
129
  inject_into_file entity_model_path,
130
- "has_many :user_invites, class_name: \"Invites::UserInvite\", dependent: :destroy\n ",
131
- before: "# add has_many associations above."
130
+ " has_many :user_invites, class_name: \"Invites::UserInvite\", dependent: :destroy\n",
131
+ before: /^\s*# add has_many associations above\.\n/
132
132
  end
133
133
 
134
134
  def create_entity_interaction
@@ -22,7 +22,7 @@ module Invites
22
22
 
23
23
  def login_path
24
24
  <% if rodauth? -%>
25
- rodauth(:<%= rodauth_config %>).login_path
25
+ rodauth.login_path
26
26
  <% else -%>
27
27
  "/login"
28
28
  <% end -%>
@@ -30,7 +30,7 @@ module Invites
30
30
 
31
31
  def current_user
32
32
  <% if rodauth? -%>
33
- rodauth(:<%= rodauth_config %>).rails_account if rodauth(:<%= rodauth_config %>).logged_in?
33
+ rodauth.rails_account if rodauth.logged_in?
34
34
  <% else -%>
35
35
  # TODO: Implement based on your authentication system
36
36
  nil
@@ -39,8 +39,7 @@ module Invites
39
39
 
40
40
  <% if rodauth? -%>
41
41
  def create_user_for_signup(email, password)
42
- rodauth_instance = rodauth(:<%= rodauth_config %>)
43
- password_hash = rodauth_instance.password_hash(password)
42
+ password_hash = rodauth.password_hash(password)
44
43
 
45
44
  if email.downcase == @invite.email.downcase
46
45
  # Email matches invitation - create verified account directly
@@ -51,18 +50,18 @@ module Invites
51
50
  )
52
51
  else
53
52
  # Different email - normal flow with verification email
54
- rodauth_instance.create_account(login: email, password: password)
53
+ rodauth.create_account(login: email, password: password)
55
54
  <%= user_model %>.find_by(email: email)
56
55
  end
57
56
  end
58
57
 
59
58
  def sign_in_user(user)
60
- rodauth(:<%= rodauth_config %>).account_from_login(user.email)
61
- rodauth(:<%= rodauth_config %>).login("signup")
59
+ rodauth.account_from_login(user.email)
60
+ rodauth.login("signup")
62
61
  end
63
62
 
64
- def rodauth(name = :<%= rodauth_config %>)
65
- request.env["rodauth.#{name}"]
63
+ def rodauth
64
+ request.env["rodauth.<%= rodauth_config %>"]
66
65
  end
67
66
  <% else -%>
68
67
  def create_user_for_signup(email, password)
@@ -43,15 +43,15 @@ module Invites
43
43
  end
44
44
 
45
45
  def current_user
46
- rodauth(:<%= rodauth_config %>).rails_account if rodauth(:<%= rodauth_config %>).logged_in?
46
+ rodauth.rails_account if rodauth.logged_in?
47
47
  end
48
48
 
49
49
  def login_path
50
- rodauth(:<%= rodauth_config %>).login_path
50
+ rodauth.login_path
51
51
  end
52
52
 
53
- def rodauth(name = :<%= rodauth_config %>)
54
- request.env["rodauth.#{name}"]
53
+ def rodauth
54
+ request.env["rodauth.<%= rodauth_config %>"]
55
55
  end
56
56
  <% else -%>
57
57
  def require_authentication
@@ -71,8 +71,21 @@ module PlutoniumGenerators
71
71
  class GeneratedAttribute < Rails::Generators::GeneratedAttribute
72
72
  class << self
73
73
  def parse(model_name, column_definition)
74
+ # Protect content inside {} from being split on colons
75
+ # e.g., "status:string{default:draft}" -> split correctly
76
+ options_content = nil
77
+ if column_definition.include?("{")
78
+ column_definition = column_definition.gsub(/\{([^}]*)\}/) do |match|
79
+ options_content = $1
80
+ "{OPTIONS}"
81
+ end
82
+ end
83
+
74
84
  name, type, index_type = column_definition.split(":")
75
85
 
86
+ # Restore options content
87
+ type = type&.sub("{OPTIONS}", "{#{options_content}}") if options_content
88
+
76
89
  # if user provided "name:index" instead of "name:string:index"
77
90
  # type should be set blank so GeneratedAttribute's constructor
78
91
  # could set it to :string
@@ -114,18 +127,68 @@ module PlutoniumGenerators
114
127
 
115
128
  private
116
129
 
130
+ # Extends Rails' parse_type_and_options to support:
131
+ # - nullable types with ? suffix: 'string?' -> null: true
132
+ # - default values: 'string{default:value}' -> default: "value"
133
+ # - class_name for references: 'belongs_to{class_name:User}' -> class_name: "User"
117
134
  def parse_type_and_options(type)
118
135
  nullable = type&.include?("?")
119
136
  type = type&.sub("?", "") if nullable
120
137
 
138
+ # Extract custom options before calling super
139
+ # Syntax: type{option:value} or type{limit,option:value}
140
+ default_value = nil
141
+ class_name_value = nil
142
+
143
+ if type&.include?("{")
144
+ # Extract default:value
145
+ if (match = type.match(/\{([^}]*default:([^,}]+)[^}]*)\}/))
146
+ default_value = match[2]
147
+ type = remove_option_from_type(type, "default", default_value)
148
+ end
149
+
150
+ # Extract class_name:Value
151
+ if (match = type.match(/\{([^}]*class_name:([^,}]+)[^}]*)\}/))
152
+ class_name_value = match[2]
153
+ type = remove_option_from_type(type, "class_name", class_name_value)
154
+ end
155
+ end
156
+
121
157
  parsed_type, parsed_options = super
122
158
 
123
159
  parsed_options[:null] = nullable ? true : false
160
+ parsed_options[:default] = coerce_default_value(default_value, parsed_type) if default_value
161
+ if class_name_value
162
+ parsed_options[:class_name] = class_name_value
163
+ parsed_options[:to_table] = class_name_value.underscore.tr("/", "_").pluralize.to_sym
164
+ end
124
165
 
125
166
  [parsed_type, parsed_options]
126
167
  end
127
168
 
128
- private
169
+ def remove_option_from_type(type, option_name, option_value)
170
+ escaped_value = Regexp.escape(option_value)
171
+ type.gsub(/\{[^}]*\}/) do |match|
172
+ content = match[1..-2] # Remove { and }
173
+ cleaned = content
174
+ .gsub(/,?\s*#{option_name}:#{escaped_value}/, "")
175
+ .gsub(/#{option_name}:#{escaped_value},?\s*/, "")
176
+ cleaned.empty? ? "" : "{#{cleaned}}"
177
+ end
178
+ end
179
+
180
+ def coerce_default_value(value, type)
181
+ case type&.to_s
182
+ when "integer"
183
+ value.to_i
184
+ when "float", "decimal"
185
+ value.to_f
186
+ when "boolean"
187
+ %w[true 1 yes].include?(value.downcase)
188
+ else
189
+ value
190
+ end
191
+ end
129
192
 
130
193
  def find_shared_namespace(model1, model2, separator: "::")
131
194
  parts1 = model1.underscore.split(separator)
@@ -12,13 +12,20 @@ module Pu
12
12
 
13
13
  desc(
14
14
  "Create a connection between a resource and a portal\n\n" \
15
- "e.g. rails g pu:res:conn todo --dest=dashboard_portal\n" \
16
- " rails g pu:res:conn profile --dest=customer_portal --singular"
15
+ "e.g. rails g pu:res:conn Todo --dest=dashboard_portal\n" \
16
+ " rails g pu:res:conn Profile --dest=customer_portal --singular\n" \
17
+ " rails g pu:res:conn Post --dest=admin_portal --policy --definition"
17
18
  )
18
19
 
19
20
  class_option :singular, type: :boolean, default: false,
20
21
  desc: "Register the resource as a singular resource (e.g., profile)"
21
22
 
23
+ class_option :policy, type: :boolean, default: false,
24
+ desc: "Create portal-specific policy even if base policy exists"
25
+
26
+ class_option :definition, type: :boolean, default: false,
27
+ desc: "Create portal-specific definition even if base definition exists"
28
+
22
29
  def start
23
30
  selected_resources = resources_selection
24
31
  @app_namespace = portal_option(:dest, prompt: "Select destination portal").camelize
@@ -33,12 +40,12 @@ module Pu
33
40
  indent("register_resource ::#{resource}#{singular_option}\n", 2),
34
41
  after: /.*Rails\.application\.routes\.draw do.*\n/
35
42
  else
36
- unless expected_parent_policy
43
+ if options[:policy] || !expected_parent_policy
37
44
  template "app/policies/resource_policy.rb",
38
45
  "packages/#{package_namespace}/app/policies/#{package_namespace}/#{resource.underscore}_policy.rb"
39
46
  end
40
47
 
41
- unless expected_parent_definition
48
+ if options[:definition] || !expected_parent_definition
42
49
  template "app/definitions/resource_definition.rb",
43
50
  "packages/#{package_namespace}/app/definitions/#{package_namespace}/#{resource.underscore}_definition.rb"
44
51
  end
@@ -5,8 +5,6 @@ module Pu
5
5
  def self.included(base)
6
6
  base.send :include, Configuration
7
7
 
8
- base.send :class_option, :primary, type: :boolean,
9
- desc: "[CONFIG] generated account is primary"
10
8
  base.send :class_option, :argon2, type: :boolean, default: false,
11
9
  desc: "[CONFIG] use Argon2 for password hashing"
12
10
  base.send :class_option, :mails, type: :boolean, default: true, desc: "[CONFIG] setup mails"
@@ -69,7 +67,7 @@ module Pu
69
67
  # Creates a hash of options to pass down options to an invoked sub generator
70
68
  def invoke_options
71
69
  # These are custom options we want to track.
72
- extra_options = %i[primary argon2 mails kitchen_sink defaults]
70
+ extra_options = %i[argon2 mails kitchen_sink defaults]
73
71
  # Append them to all the available options from our configuration
74
72
  valid_options = configuration.keys.map(&:to_sym).concat extra_options
75
73
  # Index map the list with the selection value
@@ -101,7 +99,7 @@ module Pu
101
99
  end
102
100
 
103
101
  def primary?
104
- options[:primary]
102
+ false # Primary (unnamed) accounts are not supported - all accounts must be named
105
103
  end
106
104
 
107
105
  def argon2?
@@ -146,7 +146,7 @@ module Pu
146
146
  end
147
147
 
148
148
  def default_primary_key_type
149
- if ActiveRecord.version >= Gem::Version.new("5.1")
149
+ if ActiveRecord.version >= ::Gem::Version.new("5.1")
150
150
  :bigint
151
151
  else
152
152
  :integer
@@ -156,7 +156,7 @@ module Pu
156
156
  # Active Record 7+ sets default precision to 6 for timestamp columns,
157
157
  # so we need to ensure we match this when setting the default value.
158
158
  def current_timestamp
159
- if ActiveRecord.version >= Gem::Version.new("7.0") && ["mysql2", "trilogy"].include?(activerecord_adapter) && ActiveRecord::Base.connection.supports_datetime_with_precision?
159
+ if ActiveRecord.version >= ::Gem::Version.new("7.0") && ["mysql2", "trilogy"].include?(activerecord_adapter) && ActiveRecord::Base.connection.supports_datetime_with_precision?
160
160
  "CURRENT_TIMESTAMP(6)"
161
161
  else
162
162
  "CURRENT_TIMESTAMP"
@@ -108,7 +108,7 @@ module Pu
108
108
  has_many :#{membership_table_name}, dependent: :destroy
109
109
  has_many :#{normalized_user_name.pluralize}, through: :#{membership_table_name}
110
110
  RUBY
111
- insert_into_file entity_model_path, indent(associations, 2), before: /# add has_many associations above\.\n/
111
+ inject_into_file entity_model_path, associations, before: /^\s*# add has_many associations above\.\n/
112
112
  end
113
113
 
114
114
  def add_association_to_user_model
@@ -120,7 +120,7 @@ module Pu
120
120
  has_many :#{membership_table_name}, dependent: :destroy
121
121
  has_many :#{normalized_entity_name.pluralize}, through: :#{membership_table_name}
122
122
  RUBY
123
- insert_into_file user_model_path, indent(associations, 2), before: /# add has_many associations above\.\n/
123
+ inject_into_file user_model_path, associations, before: /^\s*# add has_many associations above\.\n/
124
124
  end
125
125
 
126
126
  def find_migration_file
@@ -154,10 +154,10 @@ module Plutonium
154
154
  end
155
155
  end
156
156
  rescue ActiveRecord::RecordInvalid => e
157
- if e.record.is_a?(invite_class)
158
- flash.now[:alert] = e.record.errors.full_messages.join(", ")
157
+ flash.now[:alert] = if e.record.is_a?(invite_class)
158
+ e.record.errors.full_messages.join(", ")
159
159
  else
160
- flash.now[:alert] = "Failed to create account: #{e.record.errors.full_messages.join(", ")}"
160
+ "Failed to create account: #{e.record.errors.full_messages.join(", ")}"
161
161
  end
162
162
  render :signup
163
163
  rescue => e
@@ -33,7 +33,7 @@ module Plutonium
33
33
  @add_url ||= begin
34
34
  return unless @skip_authorization || allowed_to?(:create?, association_reflection.klass)
35
35
 
36
- url = @add_action || resource_url_for(association_reflection.klass, action: :new, parent: nil)
36
+ url = @add_action || (registered_resources.include?(association_reflection.klass) && resource_url_for(association_reflection.klass, action: :new, parent: nil))
37
37
  return unless url
38
38
 
39
39
  uri = URI(url)
@@ -1,5 +1,5 @@
1
1
  module Plutonium
2
- VERSION = "0.40.0"
2
+ VERSION = "0.41.0"
3
3
  NEXT_MAJOR_VERSION = VERSION.split(".").tap { |v|
4
4
  v[1] = v[1].to_i + 1
5
5
  v[2] = 0
data/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@radioactive-labs/plutonium",
3
- "version": "0.40.0",
3
+ "version": "0.41.0",
4
4
  "description": "Build production-ready Rails apps in minutes, not days. Convention-driven, fully customizable, AI-ready.",
5
5
  "type": "module",
6
6
  "main": "src/js/core.js",
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: plutonium
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.40.0
4
+ version: 0.41.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stefan Froelich
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-02-04 00:00:00.000000000 Z
11
+ date: 2026-02-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: zeitwerk