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,328 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "agentcode/commands/base_command"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module AgentCode
|
|
7
|
+
module Commands
|
|
8
|
+
# Generate a Postman Collection v2.1 for all registered models.
|
|
9
|
+
# Mirrors Laravel `php artisan agentcode:export-postman` exactly.
|
|
10
|
+
#
|
|
11
|
+
# Usage: rails agentcode:export_postman [--output=postman_collection.json] [--base-url=http://localhost:3000/api]
|
|
12
|
+
class ExportPostmanCommand < BaseCommand
|
|
13
|
+
attr_accessor :options
|
|
14
|
+
|
|
15
|
+
def initialize
|
|
16
|
+
super
|
|
17
|
+
@options = {
|
|
18
|
+
output: "postman_collection.json",
|
|
19
|
+
base_url: "http://localhost:3000/api",
|
|
20
|
+
project_name: nil
|
|
21
|
+
}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def perform
|
|
25
|
+
output_path = options[:output]
|
|
26
|
+
base_url = options[:base_url].chomp("/")
|
|
27
|
+
project_name = options[:project_name] || Rails.application.class.module_parent_name rescue "API"
|
|
28
|
+
|
|
29
|
+
config = AgentCode.config
|
|
30
|
+
route_groups = config.route_groups
|
|
31
|
+
all_models = config.models
|
|
32
|
+
|
|
33
|
+
has_multiple_groups = route_groups.size > 1
|
|
34
|
+
needs_org_variable = any_group_has_org_prefix?(route_groups)
|
|
35
|
+
|
|
36
|
+
variables = build_collection_variables(base_url, needs_org_variable)
|
|
37
|
+
items = []
|
|
38
|
+
|
|
39
|
+
items << build_auth_folder
|
|
40
|
+
|
|
41
|
+
if has_multiple_groups
|
|
42
|
+
# Multiple groups: Project -> Auth, GroupName -> resources
|
|
43
|
+
route_groups.each do |group_name, group_config|
|
|
44
|
+
group_models = resolve_models_for_group(all_models, config, group_name)
|
|
45
|
+
next if group_models.empty?
|
|
46
|
+
|
|
47
|
+
group_prefix = group_config[:prefix] || ""
|
|
48
|
+
|
|
49
|
+
group_items = []
|
|
50
|
+
group_models.each do |slug|
|
|
51
|
+
model_class_name = all_models[slug]
|
|
52
|
+
model_class = begin
|
|
53
|
+
model_class_name.constantize
|
|
54
|
+
rescue NameError
|
|
55
|
+
say "Model class does not exist: #{model_class_name}", :red
|
|
56
|
+
next
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
model_meta = introspect_model(model_class, slug)
|
|
60
|
+
group_items << {
|
|
61
|
+
name: slug.to_s,
|
|
62
|
+
item: build_action_folders(slug, model_meta, group_prefix)
|
|
63
|
+
}
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
if group_items.any?
|
|
67
|
+
items << {
|
|
68
|
+
name: group_name.to_s,
|
|
69
|
+
item: group_items
|
|
70
|
+
}
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
else
|
|
74
|
+
# Single group: Project -> Auth, resources (flat, backward compatible)
|
|
75
|
+
group_config = route_groups.values.first || {}
|
|
76
|
+
group_prefix = group_config[:prefix] || ""
|
|
77
|
+
group_name = route_groups.keys.first
|
|
78
|
+
|
|
79
|
+
group_models = resolve_models_for_group(all_models, config, group_name)
|
|
80
|
+
group_models.each do |slug|
|
|
81
|
+
model_class_name = all_models[slug]
|
|
82
|
+
model_class = begin
|
|
83
|
+
model_class_name.constantize
|
|
84
|
+
rescue NameError
|
|
85
|
+
say "Model class does not exist: #{model_class_name}", :red
|
|
86
|
+
next
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
model_meta = introspect_model(model_class, slug)
|
|
90
|
+
items << {
|
|
91
|
+
name: slug.to_s,
|
|
92
|
+
item: build_action_folders(slug, model_meta, group_prefix)
|
|
93
|
+
}
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
collection = {
|
|
98
|
+
info: {
|
|
99
|
+
name: project_name,
|
|
100
|
+
schema: "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
|
101
|
+
},
|
|
102
|
+
variable: variables,
|
|
103
|
+
item: items
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
json = JSON.pretty_generate(collection)
|
|
107
|
+
File.write(output_path, json)
|
|
108
|
+
|
|
109
|
+
say "Postman collection written to #{output_path}", :green
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
private
|
|
113
|
+
|
|
114
|
+
def any_group_has_org_prefix?(route_groups)
|
|
115
|
+
route_groups.any? { |_name, cfg| prefix_has_param?(cfg[:prefix] || "") }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def prefix_has_param?(prefix)
|
|
119
|
+
prefix.present? && prefix.include?(":")
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def resolve_models_for_group(all_models, config, group_name)
|
|
123
|
+
config.models_for_group(group_name)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def build_collection_variables(base_url, needs_org_prefix)
|
|
127
|
+
vars = [
|
|
128
|
+
{ key: "baseUrl", value: base_url },
|
|
129
|
+
{ key: "modelId", value: "1" },
|
|
130
|
+
{ key: "token", value: "" }
|
|
131
|
+
]
|
|
132
|
+
vars << { key: "organization", value: "organization-1" } if needs_org_prefix
|
|
133
|
+
vars
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def build_auth_folder
|
|
137
|
+
headers = default_headers
|
|
138
|
+
json_headers = headers + [{ key: "Content-Type", value: "application/json" }]
|
|
139
|
+
|
|
140
|
+
login_test = <<~JS
|
|
141
|
+
const json = pm.response.json();
|
|
142
|
+
if (json.token) {
|
|
143
|
+
pm.collectionVariables.set("token", json.token);
|
|
144
|
+
}
|
|
145
|
+
if (json.organization_slug) {
|
|
146
|
+
pm.collectionVariables.set("organization", json.organization_slug);
|
|
147
|
+
}
|
|
148
|
+
JS
|
|
149
|
+
|
|
150
|
+
{
|
|
151
|
+
name: "Authentication",
|
|
152
|
+
item: [
|
|
153
|
+
request_item("Login", "POST", "{{baseUrl}}/auth/login", {}, json_headers,
|
|
154
|
+
{ email: "user@example.com", password: "password" }, login_test.strip),
|
|
155
|
+
request_item("Logout", "POST", "{{baseUrl}}/auth/logout", {}, headers),
|
|
156
|
+
request_item("Password recover", "POST", "{{baseUrl}}/auth/password/recover", {}, json_headers,
|
|
157
|
+
{ email: "user@example.com" }),
|
|
158
|
+
request_item("Password reset", "POST", "{{baseUrl}}/auth/password/reset", {}, json_headers,
|
|
159
|
+
{ token: "{{token}}", email: "user@example.com", password: "new-password", password_confirmation: "new-password" }),
|
|
160
|
+
request_item("Register (with invitation)", "POST", "{{baseUrl}}/auth/register", {}, json_headers,
|
|
161
|
+
{ invitation_token: "{{token}}", name: "New User", password: "password", password_confirmation: "password" }),
|
|
162
|
+
request_item("Accept invitation", "POST", "{{baseUrl}}/invitations/accept", {}, json_headers,
|
|
163
|
+
{ token: "invitation-token" })
|
|
164
|
+
]
|
|
165
|
+
}
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def introspect_model(model_class, slug)
|
|
169
|
+
{
|
|
170
|
+
slug: slug,
|
|
171
|
+
except_actions: model_class.try(:agentcode_except_actions_list) || [],
|
|
172
|
+
uses_soft_deletes: model_class.try(:uses_soft_deletes?) || false,
|
|
173
|
+
allowed_filters: model_class.try(:allowed_filters) || [],
|
|
174
|
+
allowed_sorts: model_class.try(:allowed_sorts) || [],
|
|
175
|
+
allowed_fields: model_class.try(:allowed_fields) || [],
|
|
176
|
+
allowed_includes: model_class.try(:allowed_includes) || [],
|
|
177
|
+
allowed_search: model_class.try(:allowed_search) || [],
|
|
178
|
+
default_sort: model_class.try(:default_sort_field)
|
|
179
|
+
}
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def build_action_folders(slug, meta, group_prefix)
|
|
183
|
+
folders = []
|
|
184
|
+
base = base_path(slug, group_prefix)
|
|
185
|
+
except = meta[:except_actions]
|
|
186
|
+
|
|
187
|
+
folders << { name: "Index", item: build_index_requests(base, slug, meta) } unless except.include?("index")
|
|
188
|
+
folders << { name: "Show", item: build_show_requests(base, slug, meta) } unless except.include?("show")
|
|
189
|
+
folders << { name: "Store", item: build_store_requests(base) } unless except.include?("store")
|
|
190
|
+
folders << { name: "Update", item: build_update_requests(base) } unless except.include?("update")
|
|
191
|
+
folders << { name: "Destroy", item: build_destroy_requests(base) } unless except.include?("destroy")
|
|
192
|
+
|
|
193
|
+
if meta[:uses_soft_deletes]
|
|
194
|
+
folders << { name: "Trashed", item: build_trashed_requests(base) } unless except.include?("trashed")
|
|
195
|
+
folders << { name: "Restore", item: build_restore_requests(base) } unless except.include?("restore")
|
|
196
|
+
folders << { name: "Force Delete", item: build_force_delete_requests(base) } unless except.include?("forceDelete")
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
folders
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def base_path(slug, group_prefix)
|
|
203
|
+
if group_prefix.present?
|
|
204
|
+
# Replace :param route params with {{param}} Postman variables
|
|
205
|
+
postman_prefix = group_prefix.gsub(/:(\w+)/, '{{\1}}')
|
|
206
|
+
"{{baseUrl}}/#{postman_prefix}/#{slug}"
|
|
207
|
+
else
|
|
208
|
+
"{{baseUrl}}/#{slug}"
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def build_index_requests(base, slug, meta)
|
|
213
|
+
headers = default_headers
|
|
214
|
+
requests = [request_item("List all", "GET", base, {}, headers)]
|
|
215
|
+
|
|
216
|
+
meta[:allowed_filters].each do |filter|
|
|
217
|
+
requests << request_item("Filter by #{filter}", "GET", base, { "filter[#{filter}]" => "example" }, headers)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
meta[:allowed_sorts].each do |sort|
|
|
221
|
+
requests << request_item("Sort by #{sort} (asc)", "GET", base, { sort: sort.to_s }, headers)
|
|
222
|
+
requests << request_item("Sort by #{sort} (desc)", "GET", base, { sort: "-#{sort}" }, headers)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
meta[:allowed_includes].each do |inc|
|
|
226
|
+
requests << request_item("Include #{inc}", "GET", base, { include: inc.to_s }, headers)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
unless meta[:allowed_fields].empty?
|
|
230
|
+
requests << request_item("Select fields", "GET", base,
|
|
231
|
+
{ "fields[#{slug}]" => meta[:allowed_fields].first(5).join(",") }, headers)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
unless meta[:allowed_search].empty?
|
|
235
|
+
requests << request_item("Search", "GET", base, { search: "example" }, headers)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
requests << request_item("Paginate", "GET", base, { per_page: "5", page: "1" }, headers)
|
|
239
|
+
|
|
240
|
+
requests
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def build_show_requests(base, slug, meta)
|
|
244
|
+
path = "#{base}/{{modelId}}"
|
|
245
|
+
headers = default_headers
|
|
246
|
+
requests = [request_item("Show by ID", "GET", path, {}, headers)]
|
|
247
|
+
|
|
248
|
+
unless meta[:allowed_includes].empty?
|
|
249
|
+
requests << request_item("Show with include", "GET", path, { include: meta[:allowed_includes].first.to_s }, headers)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
requests
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def build_store_requests(base)
|
|
256
|
+
headers = default_headers + [{ key: "Content-Type", value: "application/json" }]
|
|
257
|
+
[request_item("Create", "POST", base, {}, headers, { title: "Example" })]
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def build_update_requests(base)
|
|
261
|
+
path = "#{base}/{{modelId}}"
|
|
262
|
+
headers = default_headers + [{ key: "Content-Type", value: "application/json" }]
|
|
263
|
+
[request_item("Update", "PUT", path, {}, headers, { title: "Updated" })]
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def build_destroy_requests(base)
|
|
267
|
+
path = "#{base}/{{modelId}}"
|
|
268
|
+
[request_item("Delete by ID", "DELETE", path, {}, default_headers)]
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def build_trashed_requests(base)
|
|
272
|
+
[request_item("List trashed", "GET", "#{base}/trashed", {}, default_headers)]
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def build_restore_requests(base)
|
|
276
|
+
[request_item("Restore by ID", "POST", "#{base}/{{modelId}}/restore", {}, default_headers)]
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def build_force_delete_requests(base)
|
|
280
|
+
[request_item("Force delete by ID", "DELETE", "#{base}/{{modelId}}/force-delete", {}, default_headers)]
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def default_headers
|
|
284
|
+
[
|
|
285
|
+
{ key: "Accept", value: "application/json" },
|
|
286
|
+
{ key: "Authorization", value: "Bearer {{token}}" }
|
|
287
|
+
]
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def request_item(name, method, path, query_params, headers, body = nil, test_script = nil)
|
|
291
|
+
query = query_params.map { |k, v| { key: k.to_s, value: v.to_s } }
|
|
292
|
+
|
|
293
|
+
raw = path
|
|
294
|
+
unless query.empty?
|
|
295
|
+
raw += "?" + query.map { |q| "#{q[:key]}=#{q[:value]}" }.join("&")
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
parts = path.split("/").reject(&:empty?)
|
|
299
|
+
url = {
|
|
300
|
+
raw: raw,
|
|
301
|
+
host: [parts.shift || "{{baseUrl}}"],
|
|
302
|
+
path: parts
|
|
303
|
+
}
|
|
304
|
+
url[:query] = query unless query.empty?
|
|
305
|
+
|
|
306
|
+
req = { method: method, header: headers, url: url }
|
|
307
|
+
|
|
308
|
+
if body && %w[POST PUT PATCH].include?(method)
|
|
309
|
+
req[:body] = {
|
|
310
|
+
mode: "raw",
|
|
311
|
+
raw: JSON.pretty_generate(body),
|
|
312
|
+
options: { raw: { language: "json" } }
|
|
313
|
+
}
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
item = { name: name }
|
|
317
|
+
if test_script
|
|
318
|
+
item[:event] = [{
|
|
319
|
+
listen: "test",
|
|
320
|
+
script: { exec: test_script.split("\n"), type: "text/javascript" }
|
|
321
|
+
}]
|
|
322
|
+
end
|
|
323
|
+
item[:request] = req
|
|
324
|
+
item
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
end
|