plutonium 0.40.0 → 0.41.1
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 +49 -7
- data/CHANGELOG.md +29 -0
- data/config/initializers/sqlite_alias.rb +50 -7
- data/docs/guides/adding-resources.md +21 -1
- data/docs/reference/generators/index.md +77 -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 +133 -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: db950a1455c885d7bdc26d86b267e95c8a7bd7cc61d5d3b2153d4225e7a40dcc
|
|
4
|
+
data.tar.gz: 6387d54982f72c298861147b167b59d768b32fa5f8ecccaa113c75b2d825df1d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f9a9dcb55e48ce7d86d8b8ee697149a96025cbacee380869188fa6d6af98981456e388ae16c141f5fc7934f44f8c1044aa0696aa9afe83e5749c8733a56bb7a5
|
|
7
|
+
data.tar.gz: 7e1c4765aa8d761ef049e9824574442b534bd1272506d2298993555bbfe25be929c58518876839f7e2be422e3cbd670c4504439f9ad05a3bf6bf14d052468f8e
|
|
@@ -83,18 +83,56 @@ 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:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
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
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### JSON/JSONB Default Values
|
|
117
|
+
|
|
118
|
+
Supports nested braces for structured defaults:
|
|
90
119
|
|
|
91
120
|
```bash
|
|
92
|
-
'
|
|
93
|
-
'
|
|
94
|
-
'
|
|
121
|
+
'metadata:jsonb{default:{}}' # Empty hash
|
|
122
|
+
'tags:jsonb{default:[]}' # Empty array
|
|
123
|
+
'settings:jsonb{default:{"theme":"dark"}}' # Object with values
|
|
124
|
+
'config:jsonb?{default:{}}' # Nullable with empty hash default
|
|
95
125
|
```
|
|
96
126
|
|
|
97
|
-
|
|
127
|
+
Default values are parsed as JSON first. If JSON parsing fails, the value is treated as a string (or coerced based on column type for integers, floats, and booleans).
|
|
128
|
+
|
|
129
|
+
### Decimal with Precision and Default
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
'amount:decimal{10,2}' # precision: 10, scale: 2
|
|
133
|
+
'price:decimal{10,2,default:0}' # with default value
|
|
134
|
+
'balance:decimal?{15,2,default:0}' # nullable with precision and default
|
|
135
|
+
```
|
|
98
136
|
|
|
99
137
|
### References/Associations
|
|
100
138
|
|
|
@@ -103,6 +141,8 @@ company:belongs_to # Required foreign key
|
|
|
103
141
|
'parent:belongs_to?' # Nullable (null: true + optional: true)
|
|
104
142
|
user:references # Same as belongs_to
|
|
105
143
|
blogging/post:belongs_to # Cross-package reference
|
|
144
|
+
'author:belongs_to{class_name:User}' # Custom class_name (author_id -> User)
|
|
145
|
+
'reviewer:belongs_to?{class_name:User}' # Nullable with class_name
|
|
106
146
|
```
|
|
107
147
|
|
|
108
148
|
Nullable references generate:
|
|
@@ -179,10 +219,12 @@ t.belongs_to :parent, null: false, foreign_key: {on_delete: :cascade}
|
|
|
179
219
|
|
|
180
220
|
### Default Values
|
|
181
221
|
|
|
222
|
+
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:
|
|
223
|
+
|
|
182
224
|
```ruby
|
|
183
225
|
t.boolean :is_active, default: true
|
|
184
226
|
t.integer :status, default: 0
|
|
185
|
-
t.
|
|
227
|
+
t.datetime :published_at, default: -> { "CURRENT_TIMESTAMP" }
|
|
186
228
|
```
|
|
187
229
|
|
|
188
230
|
## Examples
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,32 @@
|
|
|
1
|
+
## [0.41.1] - 2026-02-09
|
|
2
|
+
|
|
3
|
+
### 🚀 Features
|
|
4
|
+
|
|
5
|
+
- *(generators)* Support JSON default values for jsonb fields
|
|
6
|
+
## [0.41.0] - 2026-02-09
|
|
7
|
+
|
|
8
|
+
### 🚀 Features
|
|
9
|
+
|
|
10
|
+
- *(generators)* Add default value support and improve SQLite compatibility
|
|
11
|
+
- *(generators)* Add class_name option for belongs_to fields
|
|
12
|
+
- *(generators)* Add --policy and --definition flags to conn generator
|
|
13
|
+
|
|
14
|
+
### 🐛 Bug Fixes
|
|
15
|
+
|
|
16
|
+
- *(generators)* Fix has_many association injection pattern
|
|
17
|
+
- *(generators)* Add missing scripts to package.json in assets generator
|
|
18
|
+
- *(generators)* Remove primary account support for rodauth
|
|
19
|
+
- *(generators)* Use top-level Gem::Version to avoid namespace collision
|
|
20
|
+
- *(ui)* Check resource registration before generating association add URL
|
|
21
|
+
|
|
22
|
+
### 📚 Documentation
|
|
23
|
+
|
|
24
|
+
- Add class_name option and associations section to generator docs
|
|
25
|
+
|
|
26
|
+
### 🧪 Testing
|
|
27
|
+
|
|
28
|
+
- *(generators)* Add tests for named rodauth account configuration
|
|
29
|
+
- *(sqlite)* Move type alias tests to core and test actual migrations
|
|
1
30
|
## [0.40.0] - 2026-02-04
|
|
2
31
|
|
|
3
32
|
### 🚀 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,89 @@ 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 (supports true/false/yes/1)
|
|
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
|
+
# JSONB with empty hash default
|
|
112
|
+
rails generate pu:res:scaffold Post 'metadata:jsonb{default:{}}'
|
|
113
|
+
|
|
114
|
+
# JSONB with empty array default
|
|
115
|
+
rails generate pu:res:scaffold Post 'tags:jsonb{default:[]}'
|
|
116
|
+
|
|
117
|
+
# JSONB with object default
|
|
118
|
+
rails generate pu:res:scaffold Post 'settings:jsonb{default:{"theme":"dark"}}'
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
::: tip Shell Quoting
|
|
122
|
+
Always quote fields containing `?` or `{}` to prevent shell expansion.
|
|
123
|
+
:::
|
|
124
|
+
|
|
125
|
+
::: tip JSON Default Values
|
|
126
|
+
Default values are parsed as JSON first. This allows structured defaults like `{}` and `[]` for JSONB fields. If JSON parsing fails, the value is treated as a string (or coerced based on the column type for integers, floats, and booleans).
|
|
127
|
+
:::
|
|
128
|
+
|
|
129
|
+
#### Associations
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
# Required belongs_to
|
|
133
|
+
rails generate pu:res:scaffold Comment user:belongs_to post:belongs_to
|
|
134
|
+
|
|
135
|
+
# Nullable belongs_to
|
|
136
|
+
rails generate pu:res:scaffold Post 'parent:belongs_to?'
|
|
137
|
+
|
|
138
|
+
# Custom class_name (author_id column -> User model)
|
|
139
|
+
rails generate pu:res:scaffold Post 'author:belongs_to{class_name:User}'
|
|
140
|
+
|
|
141
|
+
# Nullable with custom class_name
|
|
142
|
+
rails generate pu:res:scaffold Post 'reviewer:belongs_to?{class_name:User}'
|
|
143
|
+
|
|
144
|
+
# Cross-package reference
|
|
145
|
+
rails generate pu:res:scaffold Comment blogging/post:belongs_to
|
|
70
146
|
```
|
|
71
147
|
|
|
72
148
|
#### 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,24 @@ 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
|
+
# Handles nested braces like "data:jsonb{default:{}}"
|
|
77
|
+
options_content = nil
|
|
78
|
+
if column_definition.include?("{")
|
|
79
|
+
options_content = extract_braced_content(column_definition)
|
|
80
|
+
if options_content
|
|
81
|
+
start_idx = column_definition.index("{")
|
|
82
|
+
end_idx = start_idx + options_content.length + 2 # +2 for { and }
|
|
83
|
+
column_definition = column_definition[0...start_idx] + "{OPTIONS}" + column_definition[end_idx..]
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
74
87
|
name, type, index_type = column_definition.split(":")
|
|
75
88
|
|
|
89
|
+
# Restore options content
|
|
90
|
+
type = type&.sub("{OPTIONS}", "{#{options_content}}") if options_content
|
|
91
|
+
|
|
76
92
|
# if user provided "name:index" instead of "name:string:index"
|
|
77
93
|
# type should be set blank so GeneratedAttribute's constructor
|
|
78
94
|
# could set it to :string
|
|
@@ -114,18 +130,134 @@ module PlutoniumGenerators
|
|
|
114
130
|
|
|
115
131
|
private
|
|
116
132
|
|
|
133
|
+
# Extends Rails' parse_type_and_options to support:
|
|
134
|
+
# - nullable types with ? suffix: 'string?' -> null: true
|
|
135
|
+
# - default values: 'string{default:value}' -> default: "value"
|
|
136
|
+
# - class_name for references: 'belongs_to{class_name:User}' -> class_name: "User"
|
|
117
137
|
def parse_type_and_options(type)
|
|
118
138
|
nullable = type&.include?("?")
|
|
119
139
|
type = type&.sub("?", "") if nullable
|
|
120
140
|
|
|
141
|
+
# Extract custom options before calling super
|
|
142
|
+
# Syntax: type{option:value} or type{limit,option:value}
|
|
143
|
+
default_value = nil
|
|
144
|
+
class_name_value = nil
|
|
145
|
+
|
|
146
|
+
if type&.include?("{")
|
|
147
|
+
options_content = extract_braced_content(type)
|
|
148
|
+
|
|
149
|
+
# Extract default:value (supports nested braces like {})
|
|
150
|
+
if options_content&.include?("default:")
|
|
151
|
+
default_value = extract_option_value(options_content, "default")
|
|
152
|
+
type = remove_option_from_type(type, "default", default_value)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Extract class_name:Value
|
|
156
|
+
if options_content&.include?("class_name:")
|
|
157
|
+
class_name_value = extract_option_value(options_content, "class_name")
|
|
158
|
+
type = remove_option_from_type(type, "class_name", class_name_value)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
121
162
|
parsed_type, parsed_options = super
|
|
122
163
|
|
|
123
164
|
parsed_options[:null] = nullable ? true : false
|
|
165
|
+
parsed_options[:default] = coerce_default_value(default_value, parsed_type) if default_value
|
|
166
|
+
if class_name_value
|
|
167
|
+
parsed_options[:class_name] = class_name_value
|
|
168
|
+
parsed_options[:to_table] = class_name_value.underscore.tr("/", "_").pluralize.to_sym
|
|
169
|
+
end
|
|
124
170
|
|
|
125
171
|
[parsed_type, parsed_options]
|
|
126
172
|
end
|
|
127
173
|
|
|
128
|
-
|
|
174
|
+
# Extracts content inside outermost braces, respecting nested braces
|
|
175
|
+
# e.g., "jsonb{default:{}}" -> "default:{}"
|
|
176
|
+
def extract_braced_content(str)
|
|
177
|
+
start_idx = str.index("{")
|
|
178
|
+
return nil unless start_idx
|
|
179
|
+
|
|
180
|
+
depth = 0
|
|
181
|
+
(start_idx...str.length).each do |i|
|
|
182
|
+
case str[i]
|
|
183
|
+
when "{"
|
|
184
|
+
depth += 1
|
|
185
|
+
when "}"
|
|
186
|
+
depth -= 1
|
|
187
|
+
return str[(start_idx + 1)...i] if depth == 0
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
nil
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Extracts option value, handling nested braces
|
|
194
|
+
# e.g., from "default:{},other:val" extracts "{}" for "default"
|
|
195
|
+
def extract_option_value(content, option_name)
|
|
196
|
+
prefix = "#{option_name}:"
|
|
197
|
+
start_idx = content.index(prefix)
|
|
198
|
+
return nil unless start_idx
|
|
199
|
+
|
|
200
|
+
value_start = start_idx + prefix.length
|
|
201
|
+
return nil if value_start >= content.length
|
|
202
|
+
|
|
203
|
+
# Check if value starts with a brace (nested structure)
|
|
204
|
+
if content[value_start] == "{"
|
|
205
|
+
depth = 0
|
|
206
|
+
(value_start...content.length).each do |i|
|
|
207
|
+
case content[i]
|
|
208
|
+
when "{"
|
|
209
|
+
depth += 1
|
|
210
|
+
when "}"
|
|
211
|
+
depth -= 1
|
|
212
|
+
return content[value_start..i] if depth == 0
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
content[value_start..]
|
|
216
|
+
else
|
|
217
|
+
# Simple value - read until comma or end
|
|
218
|
+
end_idx = content.index(",", value_start) || content.length
|
|
219
|
+
content[value_start...end_idx]
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def remove_option_from_type(type, option_name, option_value)
|
|
224
|
+
escaped_value = Regexp.escape(option_value)
|
|
225
|
+
options_content = extract_braced_content(type)
|
|
226
|
+
return type unless options_content
|
|
227
|
+
|
|
228
|
+
cleaned = options_content
|
|
229
|
+
.gsub(/,?\s*#{option_name}:#{escaped_value}/, "")
|
|
230
|
+
.gsub(/#{option_name}:#{escaped_value},?\s*/, "")
|
|
231
|
+
|
|
232
|
+
prefix = type[0...type.index("{")]
|
|
233
|
+
cleaned.empty? ? prefix : "#{prefix}{#{cleaned}}"
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Coerces default value using JSON parsing for structured types,
|
|
237
|
+
# with type-based fallback for primitives
|
|
238
|
+
def coerce_default_value(value, type)
|
|
239
|
+
# Try JSON for structured types (hashes, arrays)
|
|
240
|
+
parsed = JSON.parse(value)
|
|
241
|
+
return parsed if parsed.is_a?(Hash) || parsed.is_a?(Array)
|
|
242
|
+
|
|
243
|
+
# For primitives, use type-based coercion
|
|
244
|
+
coerce_primitive_value(value, type)
|
|
245
|
+
rescue JSON::ParserError
|
|
246
|
+
coerce_primitive_value(value, type)
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def coerce_primitive_value(value, type)
|
|
250
|
+
case type&.to_s
|
|
251
|
+
when "integer"
|
|
252
|
+
value.to_i
|
|
253
|
+
when "float", "decimal"
|
|
254
|
+
value.to_f
|
|
255
|
+
when "boolean"
|
|
256
|
+
%w[true 1 yes].include?(value.downcase)
|
|
257
|
+
else
|
|
258
|
+
value
|
|
259
|
+
end
|
|
260
|
+
end
|
|
129
261
|
|
|
130
262
|
def find_shared_namespace(model1, model2, separator: "::")
|
|
131
263
|
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.1
|
|
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
|