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.
Files changed (70) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +59 -0
  3. data/lib/agentcode/blueprint/blueprint_parser.rb +198 -0
  4. data/lib/agentcode/blueprint/blueprint_validator.rb +209 -0
  5. data/lib/agentcode/blueprint/generators/factory_generator.rb +74 -0
  6. data/lib/agentcode/blueprint/generators/policy_generator.rb +154 -0
  7. data/lib/agentcode/blueprint/generators/seeder_generator.rb +160 -0
  8. data/lib/agentcode/blueprint/generators/test_generator.rb +291 -0
  9. data/lib/agentcode/blueprint/manifest_manager.rb +81 -0
  10. data/lib/agentcode/commands/base_command.rb +57 -0
  11. data/lib/agentcode/commands/blueprint_command.rb +549 -0
  12. data/lib/agentcode/commands/export_postman_command.rb +328 -0
  13. data/lib/agentcode/commands/generate_command.rb +563 -0
  14. data/lib/agentcode/commands/install_command.rb +441 -0
  15. data/lib/agentcode/commands/invitation_link_command.rb +107 -0
  16. data/lib/agentcode/concerns/belongs_to_organization.rb +49 -0
  17. data/lib/agentcode/concerns/has_agentcode.rb +93 -0
  18. data/lib/agentcode/concerns/has_audit_trail.rb +125 -0
  19. data/lib/agentcode/concerns/has_auto_scope.rb +91 -0
  20. data/lib/agentcode/concerns/has_permissions.rb +117 -0
  21. data/lib/agentcode/concerns/has_uuid.rb +26 -0
  22. data/lib/agentcode/concerns/has_validation.rb +250 -0
  23. data/lib/agentcode/concerns/hidable_columns.rb +180 -0
  24. data/lib/agentcode/configuration.rb +98 -0
  25. data/lib/agentcode/controllers/auth_controller.rb +242 -0
  26. data/lib/agentcode/controllers/invitations_controller.rb +231 -0
  27. data/lib/agentcode/controllers/resources_controller.rb +813 -0
  28. data/lib/agentcode/engine.rb +65 -0
  29. data/lib/agentcode/mailers/invitation_mailer.rb +22 -0
  30. data/lib/agentcode/middleware/resolve_organization_from_route.rb +72 -0
  31. data/lib/agentcode/models/agentcode_model.rb +387 -0
  32. data/lib/agentcode/models/audit_log.rb +17 -0
  33. data/lib/agentcode/models/organization_invitation.rb +57 -0
  34. data/lib/agentcode/policies/invitation_policy.rb +54 -0
  35. data/lib/agentcode/policies/resource_policy.rb +197 -0
  36. data/lib/agentcode/query_builder.rb +278 -0
  37. data/lib/agentcode/railtie.rb +11 -0
  38. data/lib/agentcode/resource_scope.rb +59 -0
  39. data/lib/agentcode/routes.rb +124 -0
  40. data/lib/agentcode/tasks/agentcode.rake +39 -0
  41. data/lib/agentcode/templates/agentcode.rb +71 -0
  42. data/lib/agentcode/templates/agentcode_model.rb +104 -0
  43. data/lib/agentcode/templates/audit_trail/create_audit_logs.rb.erb +26 -0
  44. data/lib/agentcode/templates/generate/factory.rb.erb +43 -0
  45. data/lib/agentcode/templates/generate/migration.rb.erb +26 -0
  46. data/lib/agentcode/templates/generate/model.rb.erb +55 -0
  47. data/lib/agentcode/templates/generate/policy.rb.erb +52 -0
  48. data/lib/agentcode/templates/generate/scope.rb.erb +31 -0
  49. data/lib/agentcode/templates/multi_tenant/factories/organizations.rb.erb +9 -0
  50. data/lib/agentcode/templates/multi_tenant/factories/roles.rb.erb +9 -0
  51. data/lib/agentcode/templates/multi_tenant/factories/user_roles.rb.erb +10 -0
  52. data/lib/agentcode/templates/multi_tenant/factories/users.rb.erb +9 -0
  53. data/lib/agentcode/templates/multi_tenant/migrations/create_organizations.rb.erb +15 -0
  54. data/lib/agentcode/templates/multi_tenant/migrations/create_roles.rb.erb +15 -0
  55. data/lib/agentcode/templates/multi_tenant/migrations/create_user_roles.rb.erb +16 -0
  56. data/lib/agentcode/templates/multi_tenant/migrations/create_users.rb.erb +15 -0
  57. data/lib/agentcode/templates/multi_tenant/models/organization.rb.erb +18 -0
  58. data/lib/agentcode/templates/multi_tenant/models/role.rb.erb +11 -0
  59. data/lib/agentcode/templates/multi_tenant/models/user.rb.erb +14 -0
  60. data/lib/agentcode/templates/multi_tenant/models/user_role.rb.erb +9 -0
  61. data/lib/agentcode/templates/multi_tenant/policies/organization_policy.rb.erb +6 -0
  62. data/lib/agentcode/templates/multi_tenant/policies/role_policy.rb.erb +6 -0
  63. data/lib/agentcode/templates/multi_tenant/seeders/organization_seeder.rb.erb +9 -0
  64. data/lib/agentcode/templates/multi_tenant/seeders/role_seeder.rb.erb +19 -0
  65. data/lib/agentcode/templates/routes.rb +13 -0
  66. data/lib/agentcode/version.rb +5 -0
  67. data/lib/agentcode/views/lumina/invitation_mailer/invite.html.erb +29 -0
  68. data/lib/agentcode-rails.rb +3 -0
  69. data/lib/agentcode.rb +26 -0
  70. metadata +281 -0
@@ -0,0 +1,197 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AgentCode
4
+ # Base policy for all AgentCode resources.
5
+ # Mirrors the Laravel ResourcePolicy exactly.
6
+ #
7
+ # Permission format: '{slug}.{action}' (e.g., 'posts.index', 'blogs.store')
8
+ # Supports wildcards:
9
+ # - '*' grants access to everything
10
+ # - 'posts.*' grants access to all actions on posts
11
+ #
12
+ # Usage:
13
+ # class PostPolicy < AgentCode::ResourcePolicy
14
+ # # Override for custom logic:
15
+ # def update?(user, record)
16
+ # super && record.user_id == user.id
17
+ # end
18
+ #
19
+ # # Attribute permissions:
20
+ # def permitted_attributes_for_show(user)
21
+ # has_role?(user, 'admin') ? ['*'] : ['id', 'title']
22
+ # end
23
+ #
24
+ # def hidden_attributes_for_show(user)
25
+ # has_role?(user, 'admin') ? [] : ['internal_notes']
26
+ # end
27
+ #
28
+ # def permitted_attributes_for_create(user)
29
+ # has_role?(user, 'admin') ? ['*'] : ['title', 'content']
30
+ # end
31
+ #
32
+ # def permitted_attributes_for_update(user)
33
+ # has_role?(user, 'admin') ? ['*'] : ['title', 'content']
34
+ # end
35
+ # end
36
+ class ResourcePolicy
37
+ attr_reader :user, :record
38
+
39
+ def initialize(user, record)
40
+ @user = user
41
+ @record = record
42
+ end
43
+
44
+ # The resource slug used for permission checks.
45
+ # Override in child policies, or it will be auto-resolved from config.
46
+ def self.resource_slug
47
+ @resource_slug
48
+ end
49
+
50
+ def self.resource_slug=(slug)
51
+ @resource_slug = slug
52
+ end
53
+
54
+ # ------------------------------------------------------------------
55
+ # Convention-based CRUD authorization
56
+ # ------------------------------------------------------------------
57
+
58
+ def index?
59
+ check_permission("index")
60
+ end
61
+
62
+ alias_method :view_any?, :index?
63
+
64
+ def show?
65
+ check_permission("show")
66
+ end
67
+
68
+ alias_method :view?, :show?
69
+
70
+ def create?
71
+ check_permission("store")
72
+ end
73
+
74
+ def update?
75
+ check_permission("update")
76
+ end
77
+
78
+ def destroy?
79
+ check_permission("destroy")
80
+ end
81
+
82
+ alias_method :delete?, :destroy?
83
+
84
+ # ------------------------------------------------------------------
85
+ # Soft Delete authorization
86
+ # ------------------------------------------------------------------
87
+
88
+ def view_trashed?
89
+ check_permission("trashed")
90
+ end
91
+
92
+ def restore?
93
+ check_permission("restore")
94
+ end
95
+
96
+ def force_delete?
97
+ check_permission("forceDelete")
98
+ end
99
+
100
+ # ------------------------------------------------------------------
101
+ # Attribute Permissions
102
+ # ------------------------------------------------------------------
103
+
104
+ # Override to whitelist which columns are visible in API responses.
105
+ # Return ['*'] to allow all columns (default).
106
+ #
107
+ # @param user [Object, nil] The authenticated user
108
+ # @return [Array<String>]
109
+ def permitted_attributes_for_show(user)
110
+ ['*']
111
+ end
112
+
113
+ # Override to blacklist columns from API responses.
114
+ # These are always hidden, even if listed in permitted_attributes_for_show.
115
+ #
116
+ # @param user [Object, nil] The authenticated user
117
+ # @return [Array<String>]
118
+ def hidden_attributes_for_show(user)
119
+ []
120
+ end
121
+
122
+ # Override to whitelist which fields a user can submit on create.
123
+ # Return ['*'] to allow all fields (default).
124
+ #
125
+ # @param user [Object, nil] The authenticated user
126
+ # @return [Array<String>]
127
+ def permitted_attributes_for_create(user)
128
+ ['*']
129
+ end
130
+
131
+ # Override to whitelist which fields a user can submit on update.
132
+ # Return ['*'] to allow all fields (default).
133
+ #
134
+ # @param user [Object, nil] The authenticated user
135
+ # @return [Array<String>]
136
+ def permitted_attributes_for_update(user)
137
+ ['*']
138
+ end
139
+
140
+ # ------------------------------------------------------------------
141
+ # Helpers
142
+ # ------------------------------------------------------------------
143
+
144
+ # Check if the user has a specific role in the current organization.
145
+ # Convenience method for use in child policies.
146
+ #
147
+ # @param user [Object, nil] The authenticated user
148
+ # @param role_slug [String, Symbol] Role slug (e.g. 'admin', 'editor')
149
+ # @return [Boolean]
150
+ def has_role?(user, role_slug)
151
+ return false unless user
152
+ return false unless user.respond_to?(:role_slug_for_validation)
153
+
154
+ organization = current_organization
155
+ user.role_slug_for_validation(organization) == role_slug.to_s
156
+ end
157
+
158
+ private
159
+
160
+ # Check if the user has the given permission for this resource.
161
+ def check_permission(action)
162
+ return false unless user
163
+
164
+ slug = resolve_resource_slug
165
+ return false unless slug
166
+
167
+ permission = "#{slug}.#{action}"
168
+
169
+ if user.respond_to?(:has_permission?)
170
+ organization = current_organization
171
+ user.has_permission?(permission, organization)
172
+ else
173
+ # Fallback: if the user model doesn't implement has_permission?, allow
174
+ true
175
+ end
176
+ end
177
+
178
+ def resolve_resource_slug
179
+ # 1. Explicit resource_slug on the policy class
180
+ return self.class.resource_slug if self.class.resource_slug
181
+
182
+ # 2. Auto-resolve from AgentCode config
183
+ model_class = record.is_a?(Class) ? record : record.class
184
+ slug = AgentCode.config.slug_for(model_class)
185
+
186
+ # Cache for subsequent calls
187
+ self.class.resource_slug = slug if slug
188
+ slug
189
+ end
190
+
191
+ def current_organization
192
+ if defined?(RequestStore)
193
+ RequestStore.store[:agentcode_organization]
194
+ end
195
+ end
196
+ end
197
+ end
@@ -0,0 +1,278 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AgentCode
4
+ # Custom query builder that provides AgentCode's exact URL parameter format.
5
+ # Replaces Spatie QueryBuilder for Rails.
6
+ #
7
+ # Supports:
8
+ # - Filtering: ?filter[status]=published&filter[user_id]=1
9
+ # - Sorting: ?sort=-created_at,title
10
+ # - Search: ?search=term
11
+ # - Pagination: ?page=1&per_page=20
12
+ # - Fields: ?fields[posts]=id,title,status
13
+ # - Includes: ?include=user,comments
14
+ class QueryBuilder
15
+ attr_reader :scope, :model_class, :params
16
+
17
+ def initialize(model_class, params: {})
18
+ @model_class = model_class
19
+ @scope = model_class.all
20
+ @params = params
21
+ end
22
+
23
+ # Apply all query modifications based on params and model config.
24
+ def build
25
+ apply_filters
26
+ apply_default_sort
27
+ apply_sorts
28
+ apply_search
29
+ apply_fields
30
+ apply_includes
31
+ self
32
+ end
33
+
34
+ # Get the final ActiveRecord relation.
35
+ def to_scope
36
+ @scope
37
+ end
38
+
39
+ # Execute with pagination. Returns { items:, pagination: }.
40
+ def paginate(per_page: nil, page: nil)
41
+ per_page = (per_page || params[:per_page] || model_class.try(:agentcode_per_page_count) || 25).to_i
42
+ per_page = [[per_page, 1].max, 100].min # clamp between 1 and 100
43
+ page = (page || params[:page] || 1).to_i
44
+ page = [page, 1].max
45
+
46
+ total = @scope.count
47
+ last_page = (total.to_f / per_page).ceil
48
+ last_page = [last_page, 1].max
49
+
50
+ items = @scope.offset((page - 1) * per_page).limit(per_page)
51
+
52
+ {
53
+ items: items,
54
+ pagination: {
55
+ current_page: page,
56
+ last_page: last_page,
57
+ per_page: per_page,
58
+ total: total
59
+ }
60
+ }
61
+ end
62
+
63
+ private
64
+
65
+ # ------------------------------------------------------------------
66
+ # Filtering: ?filter[status]=published&filter[user_id]=1
67
+ # ------------------------------------------------------------------
68
+
69
+ def apply_filters
70
+ filter_params = params[:filter]
71
+ return unless filter_params.is_a?(ActionController::Parameters) || filter_params.is_a?(Hash)
72
+
73
+ allowed = model_class.try(:allowed_filters) || []
74
+ return if allowed.empty? && filter_params.present?
75
+
76
+ filter_params.each do |key, value|
77
+ key = key.to_s
78
+ next unless allowed.include?(key)
79
+
80
+ if value.to_s.include?(",")
81
+ # Multiple values: OR condition
82
+ values = value.to_s.split(",").map(&:strip)
83
+ values = coerce_filter_values(key, values)
84
+ @scope = @scope.where(key => values)
85
+ else
86
+ @scope = @scope.where(key => coerce_filter_value(key, value))
87
+ end
88
+ end
89
+ end
90
+
91
+ # ------------------------------------------------------------------
92
+ # Sorting: ?sort=-created_at,title
93
+ # ------------------------------------------------------------------
94
+
95
+ def apply_default_sort
96
+ return if params[:sort].present?
97
+
98
+ default = model_class.try(:default_sort_field)
99
+ return unless default
100
+
101
+ apply_sort_string(default)
102
+ end
103
+
104
+ def apply_sorts
105
+ sort_param = params[:sort]
106
+ return unless sort_param.present?
107
+
108
+ apply_sort_string(sort_param.to_s)
109
+ end
110
+
111
+ def apply_sort_string(sort_string)
112
+ allowed = model_class.try(:allowed_sorts) || []
113
+
114
+ sort_string.split(",").each do |field|
115
+ field = field.strip
116
+ if field.start_with?("-")
117
+ column = field[1..]
118
+ direction = :desc
119
+ else
120
+ column = field
121
+ direction = :asc
122
+ end
123
+
124
+ next unless allowed.empty? || allowed.include?(column)
125
+
126
+ @scope = @scope.order(column => direction)
127
+ end
128
+ end
129
+
130
+ # ------------------------------------------------------------------
131
+ # Search: ?search=term
132
+ # ------------------------------------------------------------------
133
+
134
+ def apply_search
135
+ search_term = params[:search]
136
+ return unless search_term.present?
137
+
138
+ columns = model_class.try(:allowed_search) || []
139
+ return if columns.empty?
140
+
141
+ term = "%#{search_term.to_s.downcase}%"
142
+ conditions = []
143
+ values = []
144
+
145
+ columns.each do |column|
146
+ if column.include?(".")
147
+ # Relationship search: 'user.name' -> joins(:user).where("users.name ILIKE ?", term)
148
+ parts = column.split(".", 2)
149
+ relation = parts[0]
150
+ field = parts[1]
151
+
152
+ # Determine the table name from the association
153
+ assoc = model_class.reflect_on_association(relation.to_sym)
154
+ if assoc
155
+ begin
156
+ table_name = assoc.klass.table_name
157
+ @scope = @scope.left_outer_joins(relation.to_sym)
158
+ conditions << "LOWER(#{table_name}.#{field}) LIKE ?"
159
+ values << term
160
+ rescue NoMethodError, NameError
161
+ next
162
+ end
163
+ end
164
+ else
165
+ conditions << "LOWER(#{model_class.table_name}.#{column}) LIKE ?"
166
+ values << term
167
+ end
168
+ end
169
+
170
+ return if conditions.empty?
171
+
172
+ @scope = @scope.where(conditions.join(" OR "), *values)
173
+ end
174
+
175
+ # ------------------------------------------------------------------
176
+ # Sparse fieldsets: ?fields[posts]=id,title,status
177
+ # ------------------------------------------------------------------
178
+
179
+ def apply_fields
180
+ fields_params = params[:fields]
181
+ return unless fields_params.is_a?(ActionController::Parameters) || fields_params.is_a?(Hash)
182
+
183
+ allowed = model_class.try(:allowed_fields) || []
184
+ return if allowed.empty?
185
+
186
+ # Find fields for this model's table
187
+ slug = AgentCode.config.slug_for(model_class)
188
+ model_fields = fields_params[slug.to_s] || fields_params[model_class.table_name]
189
+ return unless model_fields
190
+
191
+ requested = model_fields.to_s.split(",").map(&:strip)
192
+ # Only allow fields that are in the allowed list
193
+ valid_fields = requested.select { |f| allowed.include?(f) }
194
+
195
+ if valid_fields.any?
196
+ # Always include the primary key
197
+ valid_fields.unshift(model_class.primary_key) unless valid_fields.include?(model_class.primary_key)
198
+ @scope = @scope.select(valid_fields.map { |f| "#{model_class.table_name}.#{f}" })
199
+ end
200
+ end
201
+
202
+ # ------------------------------------------------------------------
203
+ # Eager loading: ?include=user,comments
204
+ # ------------------------------------------------------------------
205
+
206
+ def apply_includes
207
+ include_param = params[:include]
208
+ return unless include_param.present?
209
+
210
+ allowed = model_class.try(:allowed_includes) || []
211
+ return if allowed.empty?
212
+
213
+ requested = include_param.to_s.split(",").map(&:strip)
214
+
215
+ includes_list = []
216
+ requested.each do |inc|
217
+ base = resolve_base_include(inc, allowed)
218
+ next unless base
219
+
220
+ if inc.include?(".")
221
+ # Nested include: 'comments.user' -> { comments: :user }
222
+ parts = inc.split(".")
223
+ nested = parts.reverse.inject { |inner, outer| { outer.to_sym => inner.to_sym } }
224
+ includes_list << nested
225
+ elsif inc.end_with?("Count") || inc.end_with?("Exists")
226
+ # Count/Exists suffixes are handled separately in serialization
227
+ next
228
+ else
229
+ includes_list << inc.to_sym
230
+ end
231
+ end
232
+
233
+ @scope = @scope.includes(*includes_list) if includes_list.any?
234
+ end
235
+
236
+ # Coerce a single filter value to the column's type (e.g. string → integer).
237
+ def coerce_filter_value(column, value)
238
+ col = model_class.columns_hash[column]
239
+ return value unless col
240
+
241
+ case col.type
242
+ when :integer, :bigint
243
+ value.to_s.match?(/\A-?\d+\z/) ? value.to_i : value
244
+ when :float, :decimal
245
+ value.to_s.match?(/\A-?\d+(\.\d+)?\z/) ? value.to_f : value
246
+ when :boolean
247
+ ActiveModel::Type::Boolean.new.cast(value)
248
+ else
249
+ value
250
+ end
251
+ end
252
+
253
+ # Coerce an array of filter values.
254
+ def coerce_filter_values(column, values)
255
+ values.map { |v| coerce_filter_value(column, v) }
256
+ end
257
+
258
+ # Resolve an include segment to the base relationship name.
259
+ # Handles Count/Exists suffixes.
260
+ def resolve_base_include(segment, allowed)
261
+ return segment if allowed.include?(segment)
262
+
263
+ # Check Count suffix
264
+ if segment.end_with?("Count")
265
+ base = segment.sub(/Count\z/, "")
266
+ return base if allowed.include?(base)
267
+ end
268
+
269
+ # Check Exists suffix
270
+ if segment.end_with?("Exists")
271
+ base = segment.sub(/Exists\z/, "")
272
+ return base if allowed.include?(base)
273
+ end
274
+
275
+ nil
276
+ end
277
+ end
278
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AgentCode
4
+ class Railtie < ::Rails::Railtie
5
+ railtie_name :agentcode
6
+
7
+ rake_tasks do
8
+ load File.expand_path("tasks/agentcode.rake", __dir__)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AgentCode
4
+ # Base class for auto-discovered model scopes.
5
+ #
6
+ # Provides access to the current user and organization from RequestStore,
7
+ # so scopes can implement role-based or user-specific filtering.
8
+ #
9
+ # Usage:
10
+ # # app/models/scopes/project_scope.rb
11
+ # module Scopes
12
+ # class ProjectScope < AgentCode::ResourceScope
13
+ # def apply(relation)
14
+ # if role == "viewer"
15
+ # relation.where(status: "active")
16
+ # else
17
+ # relation
18
+ # end
19
+ # end
20
+ # end
21
+ # end
22
+ #
23
+ # Available methods inside +apply+:
24
+ # - +user+ — the current authenticated user (or nil)
25
+ # - +organization+ — the current organization (or nil)
26
+ # - +role+ — shortcut for the user's role slug in the current org (or nil)
27
+ #
28
+ class ResourceScope
29
+ # The current authenticated user, if any.
30
+ # @return [User, nil]
31
+ def user
32
+ RequestStore.store[:agentcode_current_user] if defined?(RequestStore)
33
+ end
34
+
35
+ # The current organization, if any.
36
+ # @return [Organization, nil]
37
+ def organization
38
+ RequestStore.store[:agentcode_organization] if defined?(RequestStore)
39
+ end
40
+
41
+ # Shortcut: the user's role slug in the current organization.
42
+ # @return [String, nil]
43
+ def role
44
+ return nil unless user && organization
45
+
46
+ if user.respond_to?(:role_slug_for_validation)
47
+ user.role_slug_for_validation(organization)
48
+ end
49
+ end
50
+
51
+ # Subclasses must implement this method.
52
+ #
53
+ # @param relation [ActiveRecord::Relation] the current query scope
54
+ # @return [ActiveRecord::Relation] the modified scope
55
+ def apply(relation)
56
+ raise NotImplementedError, "#{self.class.name} must implement #apply(relation)"
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AgentCode
4
+ # Dynamic route registration from AgentCode configuration.
5
+ # Mirrors the Laravel routes/api.php behavior exactly.
6
+ module Routes
7
+ class << self
8
+ def draw(router)
9
+ config = AgentCode.config
10
+ route_groups = config.route_groups
11
+
12
+ # Sort: literal prefixes first, parameterized (containing ':') last
13
+ sorted_groups = route_groups.sort_by { |_name, cfg| cfg[:prefix].include?(":") ? 1 : 0 }
14
+
15
+ router.instance_eval do
16
+ scope path: "api", defaults: { format: :json } do
17
+ # ---------------------------------------------------------------
18
+ # Auth Routes (always registered)
19
+ # ---------------------------------------------------------------
20
+ scope path: "auth" do
21
+ post "login", to: "agentcode/auth#login"
22
+ post "password/recover", to: "agentcode/auth#recover_password"
23
+ post "password/reset", to: "agentcode/auth#reset"
24
+ post "register", to: "agentcode/auth#register_with_invitation"
25
+ post "logout", to: "agentcode/auth#logout"
26
+ end
27
+
28
+ # ---------------------------------------------------------------
29
+ # Invitation accept (public, always registered)
30
+ # ---------------------------------------------------------------
31
+ post "invitations/accept", to: "agentcode/invitations#accept"
32
+
33
+ # ---------------------------------------------------------------
34
+ # Tenant-specific routes (invitations + nested)
35
+ # ---------------------------------------------------------------
36
+ if config.has_tenant_group?
37
+ tenant_config = route_groups[:tenant]
38
+ tenant_prefix = tenant_config[:prefix]
39
+
40
+ # Invitation routes under tenant prefix
41
+ invitation_prefix = tenant_prefix.present? ? "#{tenant_prefix}/invitations" : "invitations"
42
+
43
+ scope path: invitation_prefix do
44
+ get "/", to: "agentcode/invitations#index"
45
+ post "/", to: "agentcode/invitations#create"
46
+ post ":id/resend", to: "agentcode/invitations#resend"
47
+ delete ":id", to: "agentcode/invitations#cancel"
48
+ end
49
+
50
+ # Nested operations under tenant prefix
51
+ nested_config = config.nested
52
+ nested_path = nested_config[:path] || "nested"
53
+ nested_prefix = tenant_prefix.present? ? "#{tenant_prefix}/#{nested_path}" : nested_path
54
+
55
+ post nested_prefix, to: "agentcode/resources#nested", as: :agentcode_nested
56
+ else
57
+ # No tenant group — register nested at top level
58
+ nested_config = config.nested
59
+ nested_path = nested_config[:path] || "nested"
60
+ post nested_path, to: "agentcode/resources#nested", as: :agentcode_nested
61
+ end
62
+
63
+ # ---------------------------------------------------------------
64
+ # Per-group CRUD routes
65
+ # ---------------------------------------------------------------
66
+ sorted_groups.each do |group_name, group_config|
67
+ group_prefix = group_config[:prefix]
68
+ group_models = config.models_for_group(group_name)
69
+
70
+ group_models.each do |slug|
71
+ model_class_name = config.models[slug]
72
+ model_class = begin
73
+ model_class_name.constantize
74
+ rescue NameError
75
+ next
76
+ end
77
+
78
+ except_actions = model_class.try(:agentcode_except_actions_list) || []
79
+
80
+ route_prefix = [group_prefix, slug.to_s].reject(&:blank?).join("/")
81
+
82
+ scope path: route_prefix, defaults: { model_slug: slug.to_s, route_group: group_name.to_s } do
83
+ unless except_actions.include?("index")
84
+ get "/", to: "agentcode/resources#index", as: "agentcode_#{group_name}_#{slug}_index"
85
+ end
86
+
87
+ unless except_actions.include?("store")
88
+ post "/", to: "agentcode/resources#store", as: "agentcode_#{group_name}_#{slug}_store"
89
+ end
90
+
91
+ if model_class.try(:uses_soft_deletes?)
92
+ unless except_actions.include?("trashed")
93
+ get "trashed", to: "agentcode/resources#trashed", as: "agentcode_#{group_name}_#{slug}_trashed"
94
+ end
95
+
96
+ unless except_actions.include?("restore")
97
+ post ":id/restore", to: "agentcode/resources#restore", as: "agentcode_#{group_name}_#{slug}_restore"
98
+ end
99
+
100
+ unless except_actions.include?("forceDelete")
101
+ delete ":id/force-delete", to: "agentcode/resources#force_delete", as: "agentcode_#{group_name}_#{slug}_force_delete"
102
+ end
103
+ end
104
+
105
+ unless except_actions.include?("show")
106
+ get ":id", to: "agentcode/resources#show", as: "agentcode_#{group_name}_#{slug}_show"
107
+ end
108
+
109
+ unless except_actions.include?("update")
110
+ put ":id", to: "agentcode/resources#update", as: "agentcode_#{group_name}_#{slug}_update"
111
+ end
112
+
113
+ unless except_actions.include?("destroy")
114
+ delete ":id", to: "agentcode/resources#destroy", as: "agentcode_#{group_name}_#{slug}_destroy"
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end