agentcode 0.9.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 +7 -0
- data/README.md +59 -0
- data/lib/agentcode/blueprint/blueprint_parser.rb +198 -0
- data/lib/agentcode/blueprint/blueprint_validator.rb +209 -0
- data/lib/agentcode/blueprint/generators/factory_generator.rb +74 -0
- data/lib/agentcode/blueprint/generators/policy_generator.rb +154 -0
- data/lib/agentcode/blueprint/generators/seeder_generator.rb +160 -0
- data/lib/agentcode/blueprint/generators/test_generator.rb +291 -0
- data/lib/agentcode/blueprint/manifest_manager.rb +81 -0
- data/lib/agentcode/commands/base_command.rb +57 -0
- data/lib/agentcode/commands/blueprint_command.rb +549 -0
- data/lib/agentcode/commands/export_postman_command.rb +328 -0
- data/lib/agentcode/commands/generate_command.rb +563 -0
- data/lib/agentcode/commands/install_command.rb +441 -0
- data/lib/agentcode/commands/invitation_link_command.rb +107 -0
- data/lib/agentcode/concerns/belongs_to_organization.rb +49 -0
- data/lib/agentcode/concerns/has_agentcode.rb +93 -0
- data/lib/agentcode/concerns/has_audit_trail.rb +125 -0
- data/lib/agentcode/concerns/has_auto_scope.rb +91 -0
- data/lib/agentcode/concerns/has_permissions.rb +117 -0
- data/lib/agentcode/concerns/has_uuid.rb +26 -0
- data/lib/agentcode/concerns/has_validation.rb +250 -0
- data/lib/agentcode/concerns/hidable_columns.rb +180 -0
- data/lib/agentcode/configuration.rb +98 -0
- data/lib/agentcode/controllers/auth_controller.rb +242 -0
- data/lib/agentcode/controllers/invitations_controller.rb +231 -0
- data/lib/agentcode/controllers/resources_controller.rb +813 -0
- data/lib/agentcode/engine.rb +65 -0
- data/lib/agentcode/mailers/invitation_mailer.rb +22 -0
- data/lib/agentcode/middleware/resolve_organization_from_route.rb +72 -0
- data/lib/agentcode/models/agentcode_model.rb +387 -0
- data/lib/agentcode/models/audit_log.rb +17 -0
- data/lib/agentcode/models/organization_invitation.rb +57 -0
- data/lib/agentcode/policies/invitation_policy.rb +54 -0
- data/lib/agentcode/policies/resource_policy.rb +197 -0
- data/lib/agentcode/query_builder.rb +278 -0
- data/lib/agentcode/railtie.rb +11 -0
- data/lib/agentcode/resource_scope.rb +59 -0
- data/lib/agentcode/routes.rb +124 -0
- data/lib/agentcode/tasks/agentcode.rake +39 -0
- data/lib/agentcode/templates/agentcode.rb +71 -0
- data/lib/agentcode/templates/agentcode_model.rb +104 -0
- data/lib/agentcode/templates/audit_trail/create_audit_logs.rb.erb +26 -0
- data/lib/agentcode/templates/generate/factory.rb.erb +43 -0
- data/lib/agentcode/templates/generate/migration.rb.erb +26 -0
- data/lib/agentcode/templates/generate/model.rb.erb +55 -0
- data/lib/agentcode/templates/generate/policy.rb.erb +52 -0
- data/lib/agentcode/templates/generate/scope.rb.erb +31 -0
- data/lib/agentcode/templates/multi_tenant/factories/organizations.rb.erb +9 -0
- data/lib/agentcode/templates/multi_tenant/factories/roles.rb.erb +9 -0
- data/lib/agentcode/templates/multi_tenant/factories/user_roles.rb.erb +10 -0
- data/lib/agentcode/templates/multi_tenant/factories/users.rb.erb +9 -0
- data/lib/agentcode/templates/multi_tenant/migrations/create_organizations.rb.erb +15 -0
- data/lib/agentcode/templates/multi_tenant/migrations/create_roles.rb.erb +15 -0
- data/lib/agentcode/templates/multi_tenant/migrations/create_user_roles.rb.erb +16 -0
- data/lib/agentcode/templates/multi_tenant/migrations/create_users.rb.erb +15 -0
- data/lib/agentcode/templates/multi_tenant/models/organization.rb.erb +18 -0
- data/lib/agentcode/templates/multi_tenant/models/role.rb.erb +11 -0
- data/lib/agentcode/templates/multi_tenant/models/user.rb.erb +14 -0
- data/lib/agentcode/templates/multi_tenant/models/user_role.rb.erb +9 -0
- data/lib/agentcode/templates/multi_tenant/policies/organization_policy.rb.erb +6 -0
- data/lib/agentcode/templates/multi_tenant/policies/role_policy.rb.erb +6 -0
- data/lib/agentcode/templates/multi_tenant/seeders/organization_seeder.rb.erb +9 -0
- data/lib/agentcode/templates/multi_tenant/seeders/role_seeder.rb.erb +19 -0
- data/lib/agentcode/templates/routes.rb +13 -0
- data/lib/agentcode/version.rb +5 -0
- data/lib/agentcode/views/lumina/invitation_mailer/invite.html.erb +29 -0
- data/lib/agentcode-rails.rb +3 -0
- data/lib/agentcode.rb +26 -0
- metadata +281 -0
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "agentcode/commands/base_command"
|
|
4
|
+
|
|
5
|
+
module AgentCode
|
|
6
|
+
module Commands
|
|
7
|
+
# Interactive scaffold generator — mirrors Laravel `php artisan agentcode:generate` exactly.
|
|
8
|
+
#
|
|
9
|
+
# Usage: rails agentcode:generate (or rails agentcode:g)
|
|
10
|
+
class GenerateCommand < BaseCommand
|
|
11
|
+
def perform
|
|
12
|
+
print_banner
|
|
13
|
+
print_styled_header
|
|
14
|
+
|
|
15
|
+
type = select("What type of resource would you like to generate?") do |menu|
|
|
16
|
+
menu.choice "Model (with migration and factory)", "model"
|
|
17
|
+
menu.choice "Policy (extends ResourcePolicy)", "policy"
|
|
18
|
+
menu.choice "Scope (for ScopedDB)", "scope"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
name = ask("What is the resource name? (PascalCase singular, e.g., Post):")
|
|
22
|
+
name = name.strip.camelize
|
|
23
|
+
|
|
24
|
+
if name.blank? || name !~ /\A[A-Za-z][A-Za-z0-9]*\z/
|
|
25
|
+
say "Invalid name. Must start with a letter and contain only alphanumeric characters.", :red
|
|
26
|
+
return
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
case type
|
|
30
|
+
when "model"
|
|
31
|
+
generate_model(name)
|
|
32
|
+
when "policy"
|
|
33
|
+
generate_policy(name)
|
|
34
|
+
when "scope"
|
|
35
|
+
generate_scope(name)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
# ----------------------------------------------------------------
|
|
42
|
+
# Banner
|
|
43
|
+
# ----------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
def print_banner
|
|
46
|
+
say ""
|
|
47
|
+
|
|
48
|
+
lines = [
|
|
49
|
+
" █████╗ ██████╗ ███████╗███╗ ██╗████████╗ ██████╗ ██████╗ ██████╗ ███████╗",
|
|
50
|
+
" ██╔══██╗██╔════╝ ██╔════╝████╗ ██║╚══██╔══╝██╔════╝██╔═══██╗██╔══██╗██╔════╝",
|
|
51
|
+
" ███████║██║ ███╗█████╗ ██╔██╗ ██║ ██║ ██║ ██║ ██║██║ ██║█████╗ ",
|
|
52
|
+
" ██╔══██║██║ ██║██╔══╝ ██║╚██╗██║ ██║ ██║ ██║ ██║██║ ██║██╔══╝ ",
|
|
53
|
+
" ██║ ██║╚██████╔╝███████╗██║ ╚████║ ██║ ╚██████╗╚██████╔╝██████╔╝███████╗",
|
|
54
|
+
" ╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝"
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
gradient = [
|
|
58
|
+
[0, 255, 255], [0, 230, 200], [100, 220, 100],
|
|
59
|
+
[255, 220, 50], [255, 170, 30], [255, 120, 0]
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
lines.each_with_index do |text, i|
|
|
63
|
+
r, g, b = gradient[i]
|
|
64
|
+
$stdout.puts "\033[38;2;#{r};#{g};#{b}m#{text}\033[0m"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def print_styled_header
|
|
69
|
+
text = "+ AgentCode :: Generate :: Scaffold your resources +"
|
|
70
|
+
say ""
|
|
71
|
+
say " ┌#{"─" * (text.length + 8)}┐", :cyan
|
|
72
|
+
say " │ #{text} │", :cyan
|
|
73
|
+
say " └#{"─" * (text.length + 8)}┘", :cyan
|
|
74
|
+
say ""
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# ----------------------------------------------------------------
|
|
78
|
+
# Model generation
|
|
79
|
+
# ----------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
def generate_model(name)
|
|
82
|
+
table_name = name.underscore.pluralize
|
|
83
|
+
|
|
84
|
+
# Multi-tenant check
|
|
85
|
+
belongs_to_org = false
|
|
86
|
+
owner_relation = nil
|
|
87
|
+
is_multi_tenant = multi_tenant_enabled?
|
|
88
|
+
|
|
89
|
+
if is_multi_tenant
|
|
90
|
+
belongs_to_org = yes?("Does this model belong to an organization?")
|
|
91
|
+
|
|
92
|
+
unless belongs_to_org
|
|
93
|
+
existing_models = get_existing_models
|
|
94
|
+
if existing_models.any?
|
|
95
|
+
has_parent = yes?("Does this model have a parent that belongs to an organization?")
|
|
96
|
+
if has_parent
|
|
97
|
+
owner_model = select("Which model is the parent owner?", existing_models)
|
|
98
|
+
owner_relation = owner_model.underscore.camelize(:lower)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Collect columns
|
|
105
|
+
columns = []
|
|
106
|
+
if yes?("Would you like to define columns interactively?")
|
|
107
|
+
columns = collect_columns
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Auto-add organization_id FK
|
|
111
|
+
if belongs_to_org
|
|
112
|
+
columns.unshift({
|
|
113
|
+
name: "organization_id",
|
|
114
|
+
type: "references",
|
|
115
|
+
nullable: false,
|
|
116
|
+
unique: false,
|
|
117
|
+
index: true,
|
|
118
|
+
default: nil,
|
|
119
|
+
foreign_model: "Organization"
|
|
120
|
+
})
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Auto-add owner FK
|
|
124
|
+
if owner_relation
|
|
125
|
+
owner_fk = "#{owner_relation.underscore}_id"
|
|
126
|
+
unless columns.any? { |c| c[:name] == owner_fk }
|
|
127
|
+
columns.unshift({
|
|
128
|
+
name: owner_fk,
|
|
129
|
+
type: "references",
|
|
130
|
+
nullable: false,
|
|
131
|
+
unique: false,
|
|
132
|
+
index: true,
|
|
133
|
+
default: nil,
|
|
134
|
+
foreign_model: owner_relation.camelize
|
|
135
|
+
})
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Additional options
|
|
140
|
+
options = collect_additional_options
|
|
141
|
+
|
|
142
|
+
# Role access
|
|
143
|
+
role_access = {}
|
|
144
|
+
if options[:policy] && is_multi_tenant
|
|
145
|
+
role_access = collect_role_access(name)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Generate model
|
|
149
|
+
task("Creating #{name} model") do
|
|
150
|
+
write_model_file(name, columns, belongs_to_org, owner_relation, options)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Generate migration
|
|
154
|
+
unless columns.empty?
|
|
155
|
+
task("Creating migration for #{table_name}") do
|
|
156
|
+
write_migration_file(name, columns, options[:soft_deletes])
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Generate factory
|
|
161
|
+
task("Creating #{name} factory") do
|
|
162
|
+
write_factory_file(name, columns)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Register in config
|
|
166
|
+
task("Registering #{name} in config/initializers/agentcode.rb") do
|
|
167
|
+
register_model_in_config(name)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Generate policy
|
|
171
|
+
if options[:policy]
|
|
172
|
+
task("Generating #{name}Policy") do
|
|
173
|
+
write_policy_file(name)
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Generate scope
|
|
178
|
+
task("Generating #{name}Scope") do
|
|
179
|
+
write_scope_file(name)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
say ""
|
|
183
|
+
say "#{name} model generated successfully!", :green
|
|
184
|
+
print_created_files(name, options)
|
|
185
|
+
print_model_next_steps(name, table_name)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# ----------------------------------------------------------------
|
|
189
|
+
# Policy generation
|
|
190
|
+
# ----------------------------------------------------------------
|
|
191
|
+
|
|
192
|
+
def generate_policy(name)
|
|
193
|
+
policy_name = name.end_with?("Policy") ? name : "#{name}Policy"
|
|
194
|
+
model_name = policy_name.sub(/Policy\z/, "")
|
|
195
|
+
|
|
196
|
+
task("Generating #{policy_name}") do
|
|
197
|
+
write_policy_file(model_name)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
say ""
|
|
201
|
+
say "#{policy_name} generated successfully!", :green
|
|
202
|
+
say ""
|
|
203
|
+
say " Created: app/policies/#{policy_name.underscore}.rb"
|
|
204
|
+
say ""
|
|
205
|
+
say " Next steps:", :yellow
|
|
206
|
+
say " 1. Customize the authorization methods you need."
|
|
207
|
+
say ""
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# ----------------------------------------------------------------
|
|
211
|
+
# Scope generation
|
|
212
|
+
# ----------------------------------------------------------------
|
|
213
|
+
|
|
214
|
+
def generate_scope(name)
|
|
215
|
+
scope_name = name.end_with?("Scope") ? name : "#{name}Scope"
|
|
216
|
+
model_name = scope_name.sub(/Scope\z/, "")
|
|
217
|
+
|
|
218
|
+
task("Generating #{scope_name}") do
|
|
219
|
+
write_scope_file(model_name)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
say ""
|
|
223
|
+
say "#{scope_name} generated successfully!", :green
|
|
224
|
+
say ""
|
|
225
|
+
say " Created: app/models/scopes/#{scope_name.underscore}.rb"
|
|
226
|
+
say ""
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# ----------------------------------------------------------------
|
|
230
|
+
# Column collection
|
|
231
|
+
# ----------------------------------------------------------------
|
|
232
|
+
|
|
233
|
+
def collect_columns
|
|
234
|
+
columns = []
|
|
235
|
+
|
|
236
|
+
loop do
|
|
237
|
+
col_name = ask("Column name (snake_case, e.g., title) — leave blank to finish:")
|
|
238
|
+
break if col_name.nil? || col_name.blank?
|
|
239
|
+
|
|
240
|
+
col_type = select("Column type for '#{col_name}'") do |menu|
|
|
241
|
+
menu.choice "string (VARCHAR 255)", "string"
|
|
242
|
+
menu.choice "text (TEXT)", "text"
|
|
243
|
+
menu.choice "integer", "integer"
|
|
244
|
+
menu.choice "bigInteger", "bigint"
|
|
245
|
+
menu.choice "boolean", "boolean"
|
|
246
|
+
menu.choice "date", "date"
|
|
247
|
+
menu.choice "datetime", "datetime"
|
|
248
|
+
menu.choice "decimal (8, 2)", "decimal"
|
|
249
|
+
menu.choice "float", "float"
|
|
250
|
+
menu.choice "json", "json"
|
|
251
|
+
menu.choice "uuid", "uuid"
|
|
252
|
+
menu.choice "references (foreign key)", "references"
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
column = {
|
|
256
|
+
name: col_name,
|
|
257
|
+
type: col_type,
|
|
258
|
+
nullable: false,
|
|
259
|
+
unique: false,
|
|
260
|
+
index: false,
|
|
261
|
+
default: nil,
|
|
262
|
+
foreign_model: nil
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if col_type == "references"
|
|
266
|
+
existing = get_existing_models
|
|
267
|
+
if existing.any?
|
|
268
|
+
column[:foreign_model] = select("Which model does '#{col_name}' reference?", existing)
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
column[:nullable] = yes?("Is '#{col_name}' nullable?")
|
|
273
|
+
column[:unique] = yes?("Should '#{col_name}' be unique?")
|
|
274
|
+
|
|
275
|
+
columns << column
|
|
276
|
+
|
|
277
|
+
break unless yes?("Add another column?")
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
columns
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def collect_additional_options
|
|
284
|
+
say "Additional options:", :yellow
|
|
285
|
+
|
|
286
|
+
options = multi_select("Select additional options:") do |menu|
|
|
287
|
+
menu.choice "Soft deletes", :soft_deletes
|
|
288
|
+
menu.choice "Generate policy", :policy
|
|
289
|
+
menu.choice "Audit trail", :audit_trail
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
{
|
|
293
|
+
soft_deletes: options.include?(:soft_deletes),
|
|
294
|
+
policy: options.include?(:policy),
|
|
295
|
+
audit_trail: options.include?(:audit_trail)
|
|
296
|
+
}
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def collect_role_access(name)
|
|
300
|
+
roles = get_roles_from_config
|
|
301
|
+
return {} if roles.empty?
|
|
302
|
+
|
|
303
|
+
slug = name.underscore.pluralize
|
|
304
|
+
role_access = { "admin" => "editor" }
|
|
305
|
+
|
|
306
|
+
non_admin_roles = roles.reject { |r| r == "admin" }
|
|
307
|
+
return role_access if non_admin_roles.empty?
|
|
308
|
+
|
|
309
|
+
say ""
|
|
310
|
+
say "Define role access for #{slug}:", :cyan
|
|
311
|
+
say ""
|
|
312
|
+
|
|
313
|
+
non_admin_roles.each do |role|
|
|
314
|
+
access = select("Access level for '#{role}'") do |menu|
|
|
315
|
+
menu.choice "Editor — all actions on this model", "editor"
|
|
316
|
+
menu.choice "Viewer — read-only (index, show)", "viewer"
|
|
317
|
+
menu.choice "Writer — create & edit (index, show, store, update)", "writer"
|
|
318
|
+
menu.choice "No access", "none"
|
|
319
|
+
end
|
|
320
|
+
role_access[role] = access
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
role_access
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# ----------------------------------------------------------------
|
|
327
|
+
# File writers
|
|
328
|
+
# ----------------------------------------------------------------
|
|
329
|
+
|
|
330
|
+
def write_model_file(name, columns, belongs_to_org, owner_relation, options)
|
|
331
|
+
template = File.expand_path("../../templates/generate/model.rb.erb", __FILE__)
|
|
332
|
+
dest = Rails.root.join("app/models/#{name.underscore}.rb")
|
|
333
|
+
FileUtils.mkdir_p(File.dirname(dest))
|
|
334
|
+
|
|
335
|
+
table_name = name.underscore.pluralize
|
|
336
|
+
|
|
337
|
+
# Build data for template
|
|
338
|
+
fillable = columns.map { |c| c[:name] }.reject { |n| n == "organization_id" && belongs_to_org }
|
|
339
|
+
filter_cols = columns.reject { |c| %w[text json].include?(c[:type]) }.map { |c| c[:name] }
|
|
340
|
+
sort_cols = (columns.reject { |c| %w[text json].include?(c[:type]) }.map { |c| c[:name] } + ["created_at"]).uniq
|
|
341
|
+
field_cols = (["id"] + columns.map { |c| c[:name] } + ["created_at"]).uniq
|
|
342
|
+
include_cols = columns.select { |c| c[:type] == "references" && c[:foreign_model] }
|
|
343
|
+
.map { |c| c[:name].sub(/_id\z/, "") }
|
|
344
|
+
|
|
345
|
+
validation_rules = columns.to_h { |c| [c[:name], column_to_rails_validations(c)] }
|
|
346
|
+
|
|
347
|
+
content = ERB.new(File.read(template), trim_mode: "-").result_with_hash(
|
|
348
|
+
name: name,
|
|
349
|
+
table_name: table_name,
|
|
350
|
+
fillable: fillable,
|
|
351
|
+
filter_cols: filter_cols,
|
|
352
|
+
sort_cols: sort_cols,
|
|
353
|
+
field_cols: field_cols,
|
|
354
|
+
include_cols: include_cols,
|
|
355
|
+
validation_rules: validation_rules,
|
|
356
|
+
columns: columns,
|
|
357
|
+
belongs_to_org: belongs_to_org,
|
|
358
|
+
owner_relation: owner_relation,
|
|
359
|
+
soft_deletes: options[:soft_deletes],
|
|
360
|
+
audit_trail: options[:audit_trail]
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
File.write(dest, content)
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def write_migration_file(name, columns, soft_deletes)
|
|
367
|
+
template = File.expand_path("../../templates/generate/migration.rb.erb", __FILE__)
|
|
368
|
+
table_name = name.underscore.pluralize
|
|
369
|
+
timestamp = Time.current.strftime("%Y%m%d%H%M%S")
|
|
370
|
+
dest = Rails.root.join("db/migrate/#{timestamp}_create_#{table_name}.rb")
|
|
371
|
+
FileUtils.mkdir_p(File.dirname(dest))
|
|
372
|
+
|
|
373
|
+
content = ERB.new(File.read(template), trim_mode: "-").result_with_hash(
|
|
374
|
+
table_name: table_name,
|
|
375
|
+
class_name: "Create#{name.pluralize}",
|
|
376
|
+
columns: columns,
|
|
377
|
+
soft_deletes: soft_deletes
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
File.write(dest, content)
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def write_factory_file(name, columns)
|
|
384
|
+
template = File.expand_path("../../templates/generate/factory.rb.erb", __FILE__)
|
|
385
|
+
dest = Rails.root.join("spec/factories/#{name.underscore.pluralize}.rb")
|
|
386
|
+
FileUtils.mkdir_p(File.dirname(dest))
|
|
387
|
+
|
|
388
|
+
content = ERB.new(File.read(template), trim_mode: "-").result_with_hash(
|
|
389
|
+
name: name,
|
|
390
|
+
columns: columns
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
File.write(dest, content)
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
def write_policy_file(name)
|
|
397
|
+
template = File.expand_path("../../templates/generate/policy.rb.erb", __FILE__)
|
|
398
|
+
dest = Rails.root.join("app/policies/#{name.underscore}_policy.rb")
|
|
399
|
+
FileUtils.mkdir_p(File.dirname(dest))
|
|
400
|
+
|
|
401
|
+
content = ERB.new(File.read(template), trim_mode: "-").result_with_hash(name: name)
|
|
402
|
+
|
|
403
|
+
File.write(dest, content)
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
def write_scope_file(name)
|
|
407
|
+
template = File.expand_path("../../templates/generate/scope.rb.erb", __FILE__)
|
|
408
|
+
dest = Rails.root.join("app/models/scopes/#{name.underscore}_scope.rb")
|
|
409
|
+
FileUtils.mkdir_p(File.dirname(dest))
|
|
410
|
+
|
|
411
|
+
table_name = name.underscore.pluralize
|
|
412
|
+
content = ERB.new(File.read(template), trim_mode: "-").result_with_hash(
|
|
413
|
+
name: name,
|
|
414
|
+
table_name: table_name
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
File.write(dest, content)
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
def register_model_in_config(name)
|
|
421
|
+
config_path = Rails.root.join("config/initializers/agentcode.rb")
|
|
422
|
+
return unless File.exist?(config_path)
|
|
423
|
+
|
|
424
|
+
content = File.read(config_path)
|
|
425
|
+
slug = name.underscore.pluralize
|
|
426
|
+
|
|
427
|
+
# Check if model is already registered (non-commented line)
|
|
428
|
+
return if content.match?(/^\s+\w+\.model\s+:#{slug}\b/)
|
|
429
|
+
|
|
430
|
+
# Detect the block variable name used in the config file
|
|
431
|
+
block_var = content.match(/AgentCode\.configure\s+do\s+\|(\w+)\|/)&.captures&.first || "config"
|
|
432
|
+
|
|
433
|
+
new_entry = " #{block_var}.model :#{slug}, '#{name}'"
|
|
434
|
+
|
|
435
|
+
if content.include?("# #{block_var}.model :posts, 'Post'")
|
|
436
|
+
content = content.gsub(
|
|
437
|
+
"# #{block_var}.model :posts, 'Post'",
|
|
438
|
+
"#{new_entry}\n # #{block_var}.model :posts, 'Post'"
|
|
439
|
+
)
|
|
440
|
+
elsif content.match?(/# \w+\.model :posts, 'Post'/)
|
|
441
|
+
content = content.sub(
|
|
442
|
+
/# (\w+)\.model :posts, 'Post'/,
|
|
443
|
+
"#{new_entry}\n # \\1.model :posts, 'Post'"
|
|
444
|
+
)
|
|
445
|
+
else
|
|
446
|
+
content = content.sub(
|
|
447
|
+
/(# Register your models here.*?\n)/,
|
|
448
|
+
"\\1#{new_entry}\n"
|
|
449
|
+
)
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
File.write(config_path, content)
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
# ----------------------------------------------------------------
|
|
456
|
+
# Helpers
|
|
457
|
+
# ----------------------------------------------------------------
|
|
458
|
+
|
|
459
|
+
def column_to_rails_validations(column)
|
|
460
|
+
validations = []
|
|
461
|
+
|
|
462
|
+
case column[:type]
|
|
463
|
+
when "string"
|
|
464
|
+
validations << "length: { maximum: 255 }"
|
|
465
|
+
when "integer", "bigint"
|
|
466
|
+
validations << "numericality: { only_integer: true }"
|
|
467
|
+
when "boolean"
|
|
468
|
+
validations << "inclusion: { in: [true, false] }"
|
|
469
|
+
when "references"
|
|
470
|
+
validations << "numericality: { only_integer: true }"
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
validations
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
def column_to_faker(column)
|
|
477
|
+
case column[:name]
|
|
478
|
+
when "name", "full_name" then "Faker::Name.name"
|
|
479
|
+
when "email" then "Faker::Internet.email"
|
|
480
|
+
when "title" then "Faker::Lorem.sentence(word_count: 3)"
|
|
481
|
+
when "description", "content", "body" then "Faker::Lorem.paragraph"
|
|
482
|
+
when "slug" then "Faker::Internet.slug"
|
|
483
|
+
when "phone", "phone_number" then "Faker::PhoneNumber.phone_number"
|
|
484
|
+
when "url", "website" then "Faker::Internet.url"
|
|
485
|
+
when /\Ais_/ then "[true, false].sample"
|
|
486
|
+
else
|
|
487
|
+
case column[:type]
|
|
488
|
+
when "string" then "Faker::Lorem.sentence(word_count: 3)"
|
|
489
|
+
when "text" then "Faker::Lorem.paragraph"
|
|
490
|
+
when "integer", "bigint" then "Faker::Number.between(from: 1, to: 100)"
|
|
491
|
+
when "boolean" then "[true, false].sample"
|
|
492
|
+
when "date" then "Faker::Date.between(from: 1.year.ago, to: Date.today)"
|
|
493
|
+
when "datetime" then "Faker::Time.between(from: 1.year.ago, to: Time.current)"
|
|
494
|
+
when "decimal", "float" then "Faker::Number.decimal(l_digits: 3, r_digits: 2)"
|
|
495
|
+
when "json" then "{}"
|
|
496
|
+
when "uuid" then "SecureRandom.uuid"
|
|
497
|
+
when "references"
|
|
498
|
+
if column[:foreign_model]
|
|
499
|
+
"association :#{column[:name].sub(/_id\z/, '')}"
|
|
500
|
+
else
|
|
501
|
+
"Faker::Number.between(from: 1, to: 10)"
|
|
502
|
+
end
|
|
503
|
+
else
|
|
504
|
+
"Faker::Lorem.word"
|
|
505
|
+
end
|
|
506
|
+
end
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
def multi_tenant_enabled?
|
|
510
|
+
config_path = Rails.root.join("config/initializers/agentcode.rb")
|
|
511
|
+
return false unless File.exist?(config_path)
|
|
512
|
+
|
|
513
|
+
content = File.read(config_path)
|
|
514
|
+
content.include?("route_group :tenant")
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
def get_existing_models
|
|
518
|
+
models_path = Rails.root.join("app/models")
|
|
519
|
+
return [] unless Dir.exist?(models_path)
|
|
520
|
+
|
|
521
|
+
Dir.glob(models_path.join("*.rb")).map do |f|
|
|
522
|
+
File.basename(f, ".rb").camelize
|
|
523
|
+
end.reject { |m| m == "ApplicationRecord" }
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
def get_roles_from_config
|
|
527
|
+
config_path = Rails.root.join("config/initializers/agentcode.rb")
|
|
528
|
+
return [] unless File.exist?(config_path)
|
|
529
|
+
|
|
530
|
+
content = File.read(config_path)
|
|
531
|
+
if content =~ /roles.*?\[(.*?)\]/m
|
|
532
|
+
$1.scan(/"([^"]+)"/).flatten
|
|
533
|
+
else
|
|
534
|
+
[]
|
|
535
|
+
end
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
def print_created_files(name, options)
|
|
539
|
+
table_name = name.underscore.pluralize
|
|
540
|
+
say ""
|
|
541
|
+
say "Created files:", :yellow
|
|
542
|
+
say ""
|
|
543
|
+
say " Model app/models/#{name.underscore}.rb"
|
|
544
|
+
say " Migration db/migrate/..._create_#{table_name}.rb"
|
|
545
|
+
say " Factory spec/factories/#{table_name}.rb"
|
|
546
|
+
say " Config config/initializers/agentcode.rb (registered as '#{table_name}')"
|
|
547
|
+
say " Policy app/policies/#{name.underscore}_policy.rb" if options[:policy]
|
|
548
|
+
say " Scope app/models/scopes/#{name.underscore}_scope.rb"
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
def print_model_next_steps(name, table_name)
|
|
552
|
+
say ""
|
|
553
|
+
say "Next steps:", :yellow
|
|
554
|
+
say ""
|
|
555
|
+
say " 1. Run migrations: rails db:migrate"
|
|
556
|
+
say " 2. Review the generated model at: app/models/#{name.underscore}.rb"
|
|
557
|
+
say " 3. Run tests: rspec"
|
|
558
|
+
say " 4. Your API endpoints: GET/POST /api/#{table_name}, GET/PUT/DELETE /api/#{table_name}/{id}"
|
|
559
|
+
say ""
|
|
560
|
+
end
|
|
561
|
+
end
|
|
562
|
+
end
|
|
563
|
+
end
|