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 +4 -4
- data/.claude/skills/plutonium-create-resource/SKILL.md +36 -7
- data/CHANGELOG.md +24 -0
- data/config/initializers/sqlite_alias.rb +50 -7
- data/docs/guides/adding-resources.md +21 -1
- data/docs/reference/generators/index.md +64 -1
- data/gemfiles/rails_7.gemfile.lock +1 -1
- data/gemfiles/rails_8.0.gemfile.lock +1 -1
- data/gemfiles/rails_8.1.gemfile.lock +1 -1
- data/lib/generators/pu/core/assets/assets_generator.rb +8 -3
- data/lib/generators/pu/invites/install_generator.rb +2 -2
- data/lib/generators/pu/invites/templates/packages/invites/app/controllers/invites/user_invitations_controller.rb.tt +8 -9
- data/lib/generators/pu/invites/templates/packages/invites/app/controllers/invites/welcome_controller.rb.tt +4 -4
- data/lib/generators/pu/lib/plutonium_generators/model_generator_base.rb +64 -1
- data/lib/generators/pu/res/conn/conn_generator.rb +11 -4
- data/lib/generators/pu/rodauth/concerns/feature_selector.rb +2 -4
- data/lib/generators/pu/rodauth/migration_generator.rb +2 -2
- data/lib/generators/pu/saas/membership_generator.rb +2 -2
- data/lib/plutonium/invites/controller.rb +3 -3
- data/lib/plutonium/ui/form/components/secure_association.rb +1 -1
- data/lib/plutonium/version.rb +1 -1
- data/package.json +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 23a6720a12194c10b70fc95f5c9b4f2b8aca0906191ed4488238e13048004de4
|
|
4
|
+
data.tar.gz: bc178cbd666304c9e170cac636a10ad4c066d104d2397428dc038640ab09091a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
###
|
|
90
|
+
### PostgreSQL-Specific Types
|
|
88
91
|
|
|
89
|
-
|
|
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
|
-
'
|
|
93
|
-
'
|
|
94
|
-
'
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
11
|
-
|
|
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.
|
|
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)
|
|
@@ -58,9 +58,14 @@ module Pu
|
|
|
58
58
|
end
|
|
59
59
|
|
|
60
60
|
def replace_build_script
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
61
|
-
rodauth
|
|
59
|
+
rodauth.account_from_login(user.email)
|
|
60
|
+
rodauth.login("signup")
|
|
62
61
|
end
|
|
63
62
|
|
|
64
|
-
def rodauth
|
|
65
|
-
request.env["rodauth
|
|
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
|
|
46
|
+
rodauth.rails_account if rodauth.logged_in?
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
def login_path
|
|
50
|
-
rodauth
|
|
50
|
+
rodauth.login_path
|
|
51
51
|
end
|
|
52
52
|
|
|
53
|
-
def rodauth
|
|
54
|
-
request.env["rodauth
|
|
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
|
-
|
|
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
|
|
16
|
-
" rails g pu:res:conn
|
|
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
|
-
|
|
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
|
-
|
|
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[
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
157
|
+
flash.now[:alert] = if e.record.is_a?(invite_class)
|
|
158
|
+
e.record.errors.full_messages.join(", ")
|
|
159
159
|
else
|
|
160
|
-
|
|
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)
|
data/lib/plutonium/version.rb
CHANGED
data/package.json
CHANGED
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.
|
|
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-
|
|
11
|
+
date: 2026-02-09 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: zeitwerk
|