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,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,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
|
+
# 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(', ') %>"
|