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,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :agentcode do
4
+ desc "Install and configure AgentCode for your Rails application"
5
+ task install: :environment do
6
+ require "agentcode/commands/install_command"
7
+ AgentCode::Commands::InstallCommand.new.perform
8
+ end
9
+
10
+ desc "Generate AgentCode resources (Model, Policy, Scope)"
11
+ task generate: :environment do
12
+ require "agentcode/commands/generate_command"
13
+ AgentCode::Commands::GenerateCommand.new.perform
14
+ end
15
+
16
+ desc "Generate code from YAML blueprint files"
17
+ task blueprint: :environment do
18
+ require "agentcode/commands/blueprint_command"
19
+ AgentCode::Commands::BlueprintCommand.new.perform
20
+ end
21
+
22
+ desc "Export Postman collection for all registered models"
23
+ task export_postman: :environment do
24
+ require "agentcode/commands/export_postman_command"
25
+ cmd = AgentCode::Commands::ExportPostmanCommand.new
26
+ cmd.perform
27
+ end
28
+ end
29
+
30
+ namespace :invitation do
31
+ desc "Generate an invitation link for testing"
32
+ task :link, [:email, :organization] => :environment do |_t, args|
33
+ require "agentcode/commands/invitation_link_command"
34
+ cmd = AgentCode::Commands::InvitationLinkCommand.new
35
+ cmd.email = args[:email]
36
+ cmd.organization_identifier = args[:organization]
37
+ cmd.perform
38
+ end
39
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ # AgentCode Configuration
4
+ # This file is used to configure AgentCode for your Rails application.
5
+ # See: https://github.com/startsoft/agentcode
6
+
7
+ AgentCode.configure do |config|
8
+ # ---------------------------------------------------------------
9
+ # Models
10
+ # ---------------------------------------------------------------
11
+ # Register your models here. Each model gets automatic CRUD endpoints.
12
+ #
13
+ # config.model :posts, 'Post'
14
+ # config.model :comments, 'Comment'
15
+ # config.model :users, 'User'
16
+
17
+ # ---------------------------------------------------------------
18
+ # Route Groups (required)
19
+ # ---------------------------------------------------------------
20
+ # Define how models are exposed via different URL prefixes.
21
+ # Each group can have its own prefix, middleware, and model list.
22
+ #
23
+ # Reserved group names:
24
+ # :tenant — enables organization scoping (invitations + nested registered here)
25
+ # :public — skips authentication for routes in this group
26
+ #
27
+ # Models can be :all (all registered models) or an array of slugs.
28
+ #
29
+ # Simple non-tenant app:
30
+ # config.route_group :default, prefix: '', middleware: [], models: :all
31
+ #
32
+ # Simple multi-tenant app:
33
+ # config.route_group :tenant, prefix: ':organization', middleware: [AgentCode::Middleware::ResolveOrganizationFromRoute], models: :all
34
+ #
35
+ # Hybrid platform (customer + driver + admin + public):
36
+ # config.route_group :tenant, prefix: ':organization', middleware: [AgentCode::Middleware::ResolveOrganizationFromRoute], models: :all
37
+ # config.route_group :driver, prefix: 'driver', middleware: [], models: [:trips, :trucks]
38
+ # config.route_group :admin, prefix: 'admin', middleware: [], models: :all
39
+ # config.route_group :public, prefix: 'public', middleware: [], models: [:materials]
40
+
41
+ # config.route_group :default, prefix: '', middleware: [], models: :all
42
+
43
+ # ---------------------------------------------------------------
44
+ # Multi-tenant
45
+ # ---------------------------------------------------------------
46
+ # config.multi_tenant = {
47
+ # organization_identifier_column: 'id' # Options: 'id', 'slug', or any column
48
+ # }
49
+
50
+ # ---------------------------------------------------------------
51
+ # Invitations
52
+ # ---------------------------------------------------------------
53
+ # config.invitations = {
54
+ # expires_days: 7,
55
+ # allowed_roles: nil # nil means all roles can invite
56
+ # }
57
+
58
+ # ---------------------------------------------------------------
59
+ # Nested Operations
60
+ # ---------------------------------------------------------------
61
+ # config.nested = {
62
+ # path: 'nested',
63
+ # max_operations: 50,
64
+ # allowed_models: nil # nil = all registered models
65
+ # }
66
+
67
+ # ---------------------------------------------------------------
68
+ # Test Framework
69
+ # ---------------------------------------------------------------
70
+ # config.test_framework = 'rspec' # Options: 'rspec', 'minitest'
71
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Application-level AgentCodeModel base class.
4
+ #
5
+ # This file was published from the agentcode gem. You can customise it
6
+ # to add concerns or configuration that apply to ALL your AgentCode models.
7
+ #
8
+ # Published with: rails agentcode:install --publish-model
9
+ #
10
+ # To use:
11
+ # class Post < AgentCodeModel
12
+ # # ...
13
+ # end
14
+ #
15
+ # The parent class already includes:
16
+ # - AgentCode::HasAgentCode, AgentCode::HasValidation,
17
+ # AgentCode::HidableColumns, AgentCode::HasAutoScope
18
+ #
19
+ # Add your own concerns or override defaults below.
20
+ class AgentCodeModel < AgentCode::AgentCodeModel
21
+ self.abstract_class = true
22
+
23
+ #
24
+ # Add application-wide concerns here. For example:
25
+ #
26
+ # include AgentCode::HasAuditTrail
27
+ # include AgentCode::HasUuid
28
+ # include AgentCode::BelongsToOrganization
29
+
30
+ # -----------------------------------------------------------------
31
+ # Validation
32
+ # -----------------------------------------------------------------
33
+ #
34
+ # Use standard ActiveModel validations for type/format constraints.
35
+ # All validators should use `allow_nil: true` — presence is controlled
36
+ # by store/update rules below.
37
+ #
38
+ # validates :title, length: { maximum: 255 }, allow_nil: true
39
+ # validates :content, length: { maximum: 10_000 }, allow_nil: true
40
+ # validates :status, inclusion: { in: %w[draft published archived] }, allow_nil: true
41
+ #
42
+ # -----------------------------------------------------------------
43
+ # Store / Update rules (field allowlist + presence modifiers)
44
+ # -----------------------------------------------------------------
45
+ #
46
+ # Field permissions (which fields each role can create/update) are
47
+ # controlled by the policy, not the model. See:
48
+ # app/policies/<model_name>_policy.rb
49
+ #
50
+ # Example policy methods:
51
+ # def permitted_attributes_for_create(user)
52
+ # has_role?(user, 'admin') ? ['*'] : ['title', 'content']
53
+ # end
54
+ # def permitted_attributes_for_update(user)
55
+ # has_role?(user, 'admin') ? ['*'] : ['title', 'content']
56
+ # end
57
+
58
+ # -----------------------------------------------------------------
59
+ # Query Builder — Filtering, Sorting, Search, Includes, Fields
60
+ # -----------------------------------------------------------------
61
+ #
62
+ # agentcode_filters :status, :user_id, :category_id
63
+ # agentcode_sorts :created_at, :title, :updated_at
64
+ # self.default_sort_field = '-created_at'
65
+ # agentcode_fields :id, :title, :status, :created_at
66
+ # agentcode_includes :user, :comments, :tags
67
+ # agentcode_search :title, :content, :excerpt
68
+
69
+ # -----------------------------------------------------------------
70
+ # Pagination
71
+ # -----------------------------------------------------------------
72
+ #
73
+ # self.pagination_enabled = true
74
+ # self.agentcode_per_page_count = 25
75
+
76
+ # -----------------------------------------------------------------
77
+ # Middleware
78
+ # -----------------------------------------------------------------
79
+ #
80
+ # self.agentcode_model_middleware = ['throttle:60,1']
81
+ #
82
+ # self.agentcode_middleware_actions_map = {
83
+ # 'store' => ['verified'],
84
+ # 'update' => ['verified'],
85
+ # 'destroy' => ['admin'],
86
+ # }
87
+
88
+ # -----------------------------------------------------------------
89
+ # Route Exclusion
90
+ # -----------------------------------------------------------------
91
+ #
92
+ # # Disable delete endpoints entirely:
93
+ # self.agentcode_except_actions_list = ['destroy', 'force_delete']
94
+ #
95
+ # # Read-only API:
96
+ # self.agentcode_except_actions_list = ['store', 'update', 'destroy']
97
+
98
+ # -----------------------------------------------------------------
99
+ # Hidden Columns
100
+ # -----------------------------------------------------------------
101
+ #
102
+ # self.additional_hidden_columns = ['api_token', 'stripe_id', 'internal_notes']
103
+
104
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateAuditLogs < ActiveRecord::Migration[8.0]
4
+ def change
5
+ create_table :audit_logs do |t|
6
+ t.string :auditable_type, null: false
7
+ t.bigint :auditable_id, null: false
8
+ t.string :action, null: false
9
+ t.json :old_values
10
+ t.json :new_values
11
+ t.bigint :user_id
12
+ t.string :user_type
13
+ t.string :ip_address
14
+ t.string :user_agent
15
+ t.bigint :organization_id
16
+
17
+ t.timestamps
18
+ end
19
+
20
+ add_index :audit_logs, [:auditable_type, :auditable_id]
21
+ add_index :audit_logs, :user_id
22
+ add_index :audit_logs, :organization_id
23
+ add_index :audit_logs, :action
24
+ add_index :audit_logs, :created_at
25
+ end
26
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ FactoryBot.define do
4
+ factory :<%= name.underscore %> do
5
+ <% columns.each do |col| -%>
6
+ <% if col[:type] == "references" && col[:foreign_model] -%>
7
+ association :<%= col[:name].sub(/_id\z/, "") %>
8
+ <% elsif col[:name] =~ /\Aname\z|\Afull_name\z/ -%>
9
+ <%= col[:name] %> { Faker::Name.name }
10
+ <% elsif col[:name] == "email" -%>
11
+ email { Faker::Internet.email }
12
+ <% elsif col[:name] == "title" -%>
13
+ title { Faker::Lorem.sentence(word_count: 3) }
14
+ <% elsif col[:name] =~ /\Adescription\z|\Acontent\z|\Abody\z/ -%>
15
+ <%= col[:name] %> { Faker::Lorem.paragraph }
16
+ <% elsif col[:name] == "slug" -%>
17
+ slug { Faker::Internet.slug }
18
+ <% elsif col[:name] =~ /\Ais_/ -%>
19
+ <%= col[:name] %> { [true, false].sample }
20
+ <% elsif col[:type] == "string" -%>
21
+ <%= col[:name] %> { Faker::Lorem.sentence(word_count: 3) }
22
+ <% elsif col[:type] == "text" -%>
23
+ <%= col[:name] %> { Faker::Lorem.paragraph }
24
+ <% elsif col[:type] =~ /\Ainteger\z|\Abigint\z/ -%>
25
+ <%= col[:name] %> { Faker::Number.between(from: 1, to: 100) }
26
+ <% elsif col[:type] == "boolean" -%>
27
+ <%= col[:name] %> { [true, false].sample }
28
+ <% elsif col[:type] == "date" -%>
29
+ <%= col[:name] %> { Faker::Date.between(from: 1.year.ago, to: Date.today) }
30
+ <% elsif col[:type] == "datetime" -%>
31
+ <%= col[:name] %> { Faker::Time.between(from: 1.year.ago, to: Time.current) }
32
+ <% elsif col[:type] =~ /\Adecimal\z|\Afloat\z/ -%>
33
+ <%= col[:name] %> { Faker::Number.decimal(l_digits: 3, r_digits: 2) }
34
+ <% elsif col[:type] == "json" -%>
35
+ <%= col[:name] %> { {} }
36
+ <% elsif col[:type] == "uuid" -%>
37
+ <%= col[:name] %> { SecureRandom.uuid }
38
+ <% else -%>
39
+ <%= col[:name] %> { Faker::Lorem.word }
40
+ <% end -%>
41
+ <% end -%>
42
+ end
43
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ class <%= class_name %> < ActiveRecord::Migration[8.0]
4
+ def change
5
+ create_table :<%= table_name %> do |t|
6
+ <% columns.each do |col| -%>
7
+ <% if col[:type] == "references" -%>
8
+ t.references :<%= col[:name].sub(/_id\z/, "") %>, null: <%= col[:nullable] %>, foreign_key: true<%= col[:index] ? ", index: true" : "" %>
9
+ <% elsif col[:type] == "decimal" -%>
10
+ t.decimal :<%= col[:name] %>, precision: 8, scale: 2<%= col[:nullable] ? ", null: true" : "" %><%= col[:unique] ? ", unique: true" : "" %><%= col[:default] ? ", default: #{col[:default]}" : "" %>
11
+ <% else -%>
12
+ t.<%= col[:type] %> :<%= col[:name] %><%= col[:nullable] ? ", null: true" : "" %><%= col[:unique] ? ", unique: true" : "" %><%= col[:default] ? ", default: #{col[:default].inspect}" : "" %>
13
+ <% end -%>
14
+ <% end -%>
15
+ <% if soft_deletes -%>
16
+ t.datetime :discarded_at
17
+ <% end -%>
18
+
19
+ t.timestamps
20
+ end
21
+ <% if soft_deletes -%>
22
+
23
+ add_index :<%= table_name %>, :discarded_at
24
+ <% end -%>
25
+ end
26
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ class <%= name %> < AgentCode::AgentCodeModel
4
+ <% if belongs_to_org -%>
5
+ include AgentCode::BelongsToOrganization
6
+ <% end -%>
7
+ <% if soft_deletes -%>
8
+ include Discard::Model
9
+ <% end -%>
10
+ <% if audit_trail -%>
11
+ include AgentCode::HasAuditTrail
12
+ <% end -%>
13
+
14
+ # ---------------------------------------------------------------
15
+ # Query Builder configuration
16
+ # ---------------------------------------------------------------
17
+
18
+ <% unless filter_cols.empty? -%>
19
+ agentcode_filters <%= filter_cols.map { |c| ":#{c}" }.join(", ") %>
20
+ <% end -%>
21
+ <% unless sort_cols.empty? -%>
22
+ agentcode_sorts <%= sort_cols.map { |c| ":#{c}" }.join(", ") %>
23
+ <% end -%>
24
+ <% unless field_cols.empty? -%>
25
+ agentcode_fields <%= field_cols.map { |c| ":#{c}" }.join(", ") %>
26
+ <% end -%>
27
+ <% unless include_cols.empty? -%>
28
+ agentcode_includes <%= include_cols.map { |c| ":#{c}" }.join(", ") %>
29
+ <% end -%>
30
+
31
+ # ---------------------------------------------------------------
32
+ # Validation (ActiveModel — use allow_nil: true for all validators)
33
+ # ---------------------------------------------------------------
34
+
35
+ <% validation_rules.each do |field, validations| -%>
36
+ <% next if validations.empty? -%>
37
+ validates :<%= field %>, <%= validations.join(", ") %>, allow_nil: true
38
+ <% end -%>
39
+
40
+ # ---------------------------------------------------------------
41
+ # Field permissions are controlled by the policy.
42
+ # See: app/policies/<%= name.underscore %>_policy.rb
43
+ # ---------------------------------------------------------------
44
+
45
+ # ---------------------------------------------------------------
46
+ # Relationships
47
+ # (Organization scoping is auto-detected from belongs_to associations)
48
+ # ---------------------------------------------------------------
49
+
50
+ <% columns.select { |c| c[:type] == "references" && c[:foreign_model] }.each do |col| -%>
51
+ <% relation_name = col[:name].sub(/_id\z/, "") -%>
52
+ <% next if belongs_to_org && col[:foreign_model] == "Organization" -%>
53
+ belongs_to :<%= relation_name %>, class_name: "<%= col[:foreign_model] %>"
54
+ <% end -%>
55
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ class <%= name %>Policy < AgentCode::ResourcePolicy
4
+ # Override any method for custom authorization logic.
5
+ # Call super to compose with the default permission check.
6
+ #
7
+ # Example:
8
+ # def update?(user, record)
9
+ # super && record.user_id == user.id
10
+ # end
11
+
12
+ # def index?
13
+ # super
14
+ # end
15
+
16
+ # def show?
17
+ # super
18
+ # end
19
+
20
+ # def create?
21
+ # super
22
+ # end
23
+
24
+ # def update?
25
+ # super
26
+ # end
27
+
28
+ # def destroy?
29
+ # super
30
+ # end
31
+
32
+ # ---------------------------------------------------------------
33
+ # Attribute Permissions
34
+ # ---------------------------------------------------------------
35
+ # Override to control which fields each role can read/write.
36
+ #
37
+ # def permitted_attributes_for_show(user)
38
+ # has_role?(user, 'admin') ? ['*'] : ['id', 'title']
39
+ # end
40
+ #
41
+ # def hidden_attributes_for_show(user)
42
+ # has_role?(user, 'admin') ? [] : ['internal_notes']
43
+ # end
44
+ #
45
+ # def permitted_attributes_for_create(user)
46
+ # has_role?(user, 'admin') ? ['*'] : ['title', 'content']
47
+ # end
48
+ #
49
+ # def permitted_attributes_for_update(user)
50
+ # has_role?(user, 'admin') ? ['*'] : ['title', 'content']
51
+ # end
52
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Scopes
4
+ class <%= name %>Scope < AgentCode::ResourceScope
5
+ # Custom query scope for <%= name %>.
6
+ # Applied automatically to all <%= name %> queries via HasAutoScope.
7
+ #
8
+ # Available methods:
9
+ # - user — the current authenticated user (or nil)
10
+ # - organization — the current organization (or nil)
11
+ # - role — the user's role slug in the current org (or nil)
12
+ #
13
+ # @param relation [ActiveRecord::Relation] the current query scope
14
+ # @return [ActiveRecord::Relation] the modified scope
15
+ def apply(relation)
16
+ # Uncomment and customize your filter logic:
17
+ #
18
+ # Example: role-based filtering
19
+ # if role == "viewer"
20
+ # relation = relation.where(status: "active")
21
+ # end
22
+ #
23
+ # Example: user-scoped records
24
+ # if user
25
+ # relation = relation.where(user_id: user.id)
26
+ # end
27
+
28
+ relation
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ FactoryBot.define do
4
+ factory :organization do
5
+ name { Faker::Company.name }
6
+ slug { Faker::Internet.slug }
7
+ description { Faker::Lorem.sentence }
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ FactoryBot.define do
4
+ factory :role do
5
+ name { Faker::Job.title }
6
+ slug { Faker::Internet.slug }
7
+ description { Faker::Lorem.sentence }
8
+ end
9
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ FactoryBot.define do
4
+ factory :user_role do
5
+ association :user
6
+ association :organization
7
+ association :role
8
+ permissions { [] }
9
+ end
10
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ FactoryBot.define do
4
+ factory :user do
5
+ name { Faker::Name.name }
6
+ email { Faker::Internet.email }
7
+ password { "password123" }
8
+ end
9
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateOrganizations < ActiveRecord::Migration[8.0]
4
+ def change
5
+ create_table :organizations do |t|
6
+ t.string :name, null: false
7
+ t.string :slug, null: false
8
+ t.text :description
9
+
10
+ t.timestamps
11
+ end
12
+
13
+ add_index :organizations, :slug, unique: true
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateRoles < ActiveRecord::Migration[8.0]
4
+ def change
5
+ create_table :roles do |t|
6
+ t.string :name, null: false
7
+ t.string :slug, null: false
8
+ t.text :description
9
+
10
+ t.timestamps
11
+ end
12
+
13
+ add_index :roles, :slug, unique: true
14
+ end
15
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateUserRoles < ActiveRecord::Migration[8.0]
4
+ def change
5
+ create_table :user_roles do |t|
6
+ t.references :user, null: false, foreign_key: true
7
+ t.references :organization, null: false, foreign_key: true
8
+ t.references :role, null: false, foreign_key: true
9
+ t.json :permissions, default: []
10
+
11
+ t.timestamps
12
+ end
13
+
14
+ add_index :user_roles, [:user_id, :organization_id], unique: true
15
+ end
16
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateUsers < ActiveRecord::Migration[8.0]
4
+ def change
5
+ create_table :users do |t|
6
+ t.string :name, null: false
7
+ t.string :email, null: false
8
+ t.string :password_digest, null: false
9
+
10
+ t.timestamps
11
+ end
12
+
13
+ add_index :users, :email, unique: true
14
+ end
15
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Organization < ApplicationRecord
4
+ has_many :user_roles, dependent: :destroy
5
+ has_many :users, through: :user_roles
6
+ has_many :roles, through: :user_roles
7
+
8
+ validates :name, presence: true
9
+ validates :slug, presence: true, uniqueness: true
10
+
11
+ before_validation :generate_slug, on: :create
12
+
13
+ private
14
+
15
+ def generate_slug
16
+ self.slug ||= name&.parameterize
17
+ end
18
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Role < ApplicationRecord
4
+ ROLES = <%= roles.inspect %>.freeze
5
+
6
+ has_many :user_roles, dependent: :destroy
7
+ has_many :users, through: :user_roles
8
+
9
+ validates :name, presence: true
10
+ validates :slug, presence: true, uniqueness: true
11
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ class User < ApplicationRecord
4
+ include AgentCode::HasPermissions
5
+
6
+ has_secure_password
7
+
8
+ has_many :user_roles, dependent: :destroy
9
+ has_many :organizations, through: :user_roles
10
+ has_many :roles, through: :user_roles
11
+
12
+ validates :name, presence: true
13
+ validates :email, presence: true, uniqueness: true
14
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ class UserRole < ApplicationRecord
4
+ belongs_to :user
5
+ belongs_to :organization
6
+ belongs_to :role
7
+
8
+ validates :user_id, uniqueness: { scope: :organization_id }
9
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ class OrganizationPolicy < AgentCode::ResourcePolicy
4
+ # Organizations are managed by admins only.
5
+ # Override methods as needed.
6
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ class RolePolicy < AgentCode::ResourcePolicy
4
+ # Roles are managed by admins only.
5
+ # Override methods as needed.
6
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Seed a default organization
4
+ org = Organization.find_or_create_by!(slug: "default") do |o|
5
+ o.name = "Default Organization"
6
+ o.description = "Default organization created during setup"
7
+ end
8
+
9
+ puts "Organization seeded: #{org.name} (#{org.slug})"
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Seed roles for multi-tenant setup
4
+ <% roles.each do |role| -%>
5
+ <% role_name = role.capitalize -%>
6
+ <% description = case role
7
+ when "admin" then "Administrator role with full access"
8
+ when "editor" then "Editor role with create, read, and update access"
9
+ when "viewer" then "Viewer role with read-only access"
10
+ when "writer" then "Writer role with create and edit access"
11
+ else "#{role_name} role"
12
+ end -%>
13
+ Role.find_or_create_by!(slug: "<%= role %>") do |r|
14
+ r.name = "<%= role_name %>"
15
+ r.description = "<%= description %>"
16
+ end
17
+ <% end -%>
18
+
19
+ puts "Roles seeded: <%= roles.join(', ') %>"