forest_admin_rails 1.0.0.pre.beta.24 → 1.44.4
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 +4 -4
- data/app/controllers/forest_admin_rails/forest_controller.rb +86 -27
- data/config/initializers/forest_admin_error_subscriber.rb +12 -0
- data/config/initializers/forest_admin_route_cache.rb +11 -0
- data/config/routes.rb +22 -2
- data/lib/forest_admin/types.rb +202 -0
- data/lib/forest_admin_rails/engine.rb +83 -9
- data/lib/forest_admin_rails/plugins/active_storage.rb +130 -0
- data/lib/forest_admin_rails/version.rb +1 -1
- data/lib/forest_admin_rails.rb +18 -3
- data/lib/generators/forest_admin_rails/install_generator.rb +1 -1
- data/lib/tasks/forest_admin_rails_tasks.rake +43 -4
- metadata +127 -12
- data/README.md +0 -28
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d50d0efa3697bb50f9245de346218cba23f20d4656d1b911e1288d8458f36fe7
|
|
4
|
+
data.tar.gz: 37cc1bb7b839d70a1a657b4a789c09c91c25f7e4d88301bfc93d00f1d63e4f63
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e9e9afe36b09214d3c7e7a2554ddc45bd57c5e2362ecfcdb91038b1cdc87387f2f06833ec2919faecfce28f24da563bbe521d7fa8497a982067ac9dda530bafd
|
|
7
|
+
data.tar.gz: 8e5bb1355764c22cb9d15873dfc2ece91a4efaf9d2b6271729863dea4b194706dbc44ba5dadfb228d79aecd51cfdc0b971d105068e09a5cbffb98c89a1b3c0cb
|
|
@@ -1,14 +1,29 @@
|
|
|
1
|
+
require 'jsonapi-serializers'
|
|
2
|
+
require 'openid_connect'
|
|
3
|
+
|
|
1
4
|
module ForestAdminRails
|
|
2
5
|
class ForestController < ActionController::Base
|
|
6
|
+
include ForestAdminAgent::Http
|
|
7
|
+
|
|
3
8
|
skip_forgery_protection
|
|
4
9
|
|
|
5
10
|
def index
|
|
6
|
-
if ForestAdminAgent::Http::Router.
|
|
7
|
-
route = ForestAdminAgent::Http::Router.
|
|
11
|
+
if ForestAdminAgent::Http::Router.cached_routes.key? params['route_alias']
|
|
12
|
+
route = ForestAdminAgent::Http::Router.cached_routes[params['route_alias']]
|
|
8
13
|
|
|
9
14
|
begin
|
|
10
|
-
forest_response route[:closure].call(
|
|
11
|
-
|
|
15
|
+
forest_response route[:closure].call(
|
|
16
|
+
{
|
|
17
|
+
params: params.to_unsafe_h,
|
|
18
|
+
headers: request.headers.to_h,
|
|
19
|
+
# Raw request context for verbatim-forwarding routes (workflow executor proxy);
|
|
20
|
+
# other routes ignore these keys.
|
|
21
|
+
method: request.request_method,
|
|
22
|
+
query_string: request.query_string,
|
|
23
|
+
body: request.raw_post
|
|
24
|
+
}
|
|
25
|
+
)
|
|
26
|
+
rescue StandardError, JSONAPI::Serializer::Error => e
|
|
12
27
|
exception_handler e
|
|
13
28
|
end
|
|
14
29
|
else
|
|
@@ -17,35 +32,79 @@ module ForestAdminRails
|
|
|
17
32
|
end
|
|
18
33
|
|
|
19
34
|
def forest_response(data = {})
|
|
20
|
-
|
|
35
|
+
if data[:content].is_a?(Hash)
|
|
36
|
+
# Handle streaming responses (NEW)
|
|
37
|
+
return handle_streaming_response(data) if data.dig(:content, :type) == 'Stream'
|
|
38
|
+
|
|
39
|
+
if data.dig(:content, :type) == 'File'
|
|
40
|
+
response.headers['Access-Control-Expose-Headers'] = 'Content-Disposition'
|
|
41
|
+
return send_data data[:content][:stream], filename: data[:content][:name], type: data[:content][:mime_type],
|
|
42
|
+
disposition: 'attachment'
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
if data.dig(:content, :headers)
|
|
46
|
+
response.headers.merge!(data[:content][:headers] || {})
|
|
47
|
+
data[:content].delete(:headers)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Proxy routes expose response headers at the root (their `content` is the rendered body,
|
|
52
|
+
# so headers can't nest under it like Stream/File do).
|
|
53
|
+
response.headers.merge!(data[:headers]) if data[:headers].is_a?(Hash)
|
|
54
|
+
|
|
55
|
+
respond_to do |format|
|
|
56
|
+
format.json { render json: data[:content], status: data[:status] || data[:content][:status] || 200 }
|
|
57
|
+
format.csv { render plain: data[:content][:export], status: 200, filename: data[:filename] }
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
# Handle streaming response (enumerator-based)
|
|
64
|
+
def handle_streaming_response(data)
|
|
65
|
+
enumerator = data[:content][:enumerator]
|
|
66
|
+
headers = data[:content][:headers] || {}
|
|
67
|
+
|
|
68
|
+
# Merge headers
|
|
69
|
+
response.headers.merge!(headers)
|
|
70
|
+
|
|
71
|
+
# Set response status
|
|
72
|
+
response.status = data[:status] || 200
|
|
73
|
+
|
|
74
|
+
# Stream the enumerator
|
|
75
|
+
# Rails will automatically use chunked transfer encoding
|
|
76
|
+
self.response_body = enumerator
|
|
21
77
|
end
|
|
22
78
|
|
|
23
79
|
def exception_handler(exception)
|
|
24
|
-
|
|
25
|
-
data = {
|
|
26
|
-
error: exception.error,
|
|
27
|
-
error_description: exception.error_description,
|
|
28
|
-
state: exception.state
|
|
29
|
-
}
|
|
30
|
-
else
|
|
31
|
-
data = {
|
|
32
|
-
errors: [
|
|
33
|
-
{
|
|
34
|
-
name: exception.name,
|
|
35
|
-
detail: exception.message,
|
|
36
|
-
status: exception.try(:status)
|
|
37
|
-
}
|
|
38
|
-
]
|
|
39
|
-
}
|
|
80
|
+
http_exception = ErrorTranslator.translate(exception)
|
|
40
81
|
|
|
41
|
-
|
|
82
|
+
response.headers.merge!(http_exception.custom_headers || {})
|
|
42
83
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
84
|
+
data = case exception
|
|
85
|
+
when ForestAdminAgent::Http::Exceptions::AuthenticationOpenIdClient,
|
|
86
|
+
OpenIDConnect::Exception
|
|
87
|
+
{
|
|
88
|
+
error: exception.message,
|
|
89
|
+
error_description: exception.response,
|
|
90
|
+
state: http_exception.status
|
|
91
|
+
}
|
|
92
|
+
else
|
|
93
|
+
{
|
|
94
|
+
errors: [
|
|
95
|
+
{
|
|
96
|
+
name: http_exception.name,
|
|
97
|
+
detail: http_exception.message,
|
|
98
|
+
status: http_exception.status,
|
|
99
|
+
data: http_exception.data
|
|
100
|
+
}.compact
|
|
101
|
+
]
|
|
102
|
+
}
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
ForestAdminAgent::Facades::Container.logger.log('Error', exception.full_message) if http_exception.status >= 500
|
|
47
106
|
|
|
48
|
-
render json: data, status:
|
|
107
|
+
render json: data, status: http_exception.status
|
|
49
108
|
end
|
|
50
109
|
end
|
|
51
110
|
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
class ForestAdminErrorSubscriber
|
|
2
|
+
SEVERITY_TO_LEVEL = {
|
|
3
|
+
error: 'Error',
|
|
4
|
+
warning: 'Warn',
|
|
5
|
+
info: 'Info'
|
|
6
|
+
}.freeze
|
|
7
|
+
|
|
8
|
+
def report(error, handled:, severity:, context:, source: nil)
|
|
9
|
+
level = SEVERITY_TO_LEVEL.fetch(severity, 'Error')
|
|
10
|
+
ForestAdminAgent::Facades::Container.logger.log(level, "[ForestAdmin] #{error.full_message}")
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Route pre-caching has been moved to engine.rb after agent.build() completes
|
|
2
|
+
#
|
|
3
|
+
# This initializer is kept for informational purposes only
|
|
4
|
+
if defined?(ForestAdminAgent::Http::Router)
|
|
5
|
+
cache_disabled = ForestAdminAgent::Http::Router.cache_disabled?
|
|
6
|
+
|
|
7
|
+
if cache_disabled
|
|
8
|
+
Rails.logger.warn('[ForestAdmin] Route caching is DISABLED - this will impact performance')
|
|
9
|
+
Rails.logger.warn('[ForestAdmin] To enable caching, remove disable_route_cache: true from configuration')
|
|
10
|
+
end
|
|
11
|
+
end
|
data/config/routes.rb
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
ForestAdminRails::Engine.routes.draw do
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
next if defined?(Rake) && Rake.respond_to?(:application) && Rake.application&.top_level_tasks&.any?
|
|
3
|
+
|
|
4
|
+
# Don't load routes if the agent hasn't been set up (e.g., during tests)
|
|
5
|
+
next unless ForestAdminAgent::Builder::AgentFactory.instance.container
|
|
6
|
+
|
|
7
|
+
scope '/forest' do
|
|
8
|
+
# Use cached_routes to avoid recomputing routes during Rails route initialization
|
|
9
|
+
ForestAdminAgent::Http::Router.cached_routes.each do |name, agent_route|
|
|
10
|
+
match agent_route[:uri],
|
|
11
|
+
defaults: { format: agent_route[:format] },
|
|
12
|
+
to: 'forest#index',
|
|
13
|
+
via: agent_route[:method],
|
|
14
|
+
as: name,
|
|
15
|
+
route_alias: name,
|
|
16
|
+
constraints: { id: /[a-zA-Z0-9.-]*+/ }
|
|
17
|
+
end
|
|
18
|
+
rescue StandardError => e
|
|
19
|
+
if defined?(Rails) && Rails.logger
|
|
20
|
+
Rails.logger.error("[ForestAdmin] CRITICAL: Failed to initialize routes: #{e.class} - #{e.message}")
|
|
21
|
+
Rails.logger.error(e.backtrace.join("\n"))
|
|
22
|
+
end
|
|
23
|
+
raise e
|
|
4
24
|
end
|
|
5
25
|
end
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Ensure dependencies are fully loaded before referencing their constants
|
|
4
|
+
require 'forest_admin_datasource_toolkit'
|
|
5
|
+
require 'forest_admin_datasource_customizer'
|
|
6
|
+
require 'forest_admin_agent'
|
|
7
|
+
|
|
8
|
+
# ForestAdmin::Types provides a convenient way to access commonly-used classes
|
|
9
|
+
# from ForestAdmin packages without requiring multiple includes.
|
|
10
|
+
#
|
|
11
|
+
# Usage:
|
|
12
|
+
# include ForestAdmin::Types
|
|
13
|
+
#
|
|
14
|
+
# # Now you can use short class names:
|
|
15
|
+
# ConditionTreeLeaf.new('status', Operators::EQUAL, 'active')
|
|
16
|
+
# Filter.new(condition_tree: tree)
|
|
17
|
+
# Aggregation.new(operation: 'Count', field: 'id')
|
|
18
|
+
#
|
|
19
|
+
# # In action blocks, result_builder is an ActionResultBuilder:
|
|
20
|
+
# result_builder.success(message: 'Done!')
|
|
21
|
+
# result_builder.file(content: data, name: 'export.csv')
|
|
22
|
+
#
|
|
23
|
+
# # In chart blocks, result_builder is a ChartResultBuilder:
|
|
24
|
+
# result_builder.value(42)
|
|
25
|
+
# result_builder.distribution({ 'Active' => 10, 'Inactive' => 5 })
|
|
26
|
+
#
|
|
27
|
+
# # Factory classes for building complex objects:
|
|
28
|
+
# tree = ConditionTreeFactory.from_plain_object({ field: 'status', operator: 'equal', value: 'active' })
|
|
29
|
+
# filter = ConditionTreeFactory.match_ids(collection, [1, 2, 3])
|
|
30
|
+
# combined = ConditionTreeFactory.intersect([tree1, tree2])
|
|
31
|
+
# projection = ProjectionFactory.all(collection)
|
|
32
|
+
# sort = SortFactory.by_primary_keys(collection)
|
|
33
|
+
#
|
|
34
|
+
# # Exception handling in hooks and actions:
|
|
35
|
+
# raise ValidationError.new('Invalid input') if invalid?
|
|
36
|
+
# raise ForbiddenError.new('Access denied') unless authorized?
|
|
37
|
+
# raise UnprocessableError.new('Cannot process request')
|
|
38
|
+
#
|
|
39
|
+
# # Context classes for different handlers:
|
|
40
|
+
# # - ChartContext: context.get_record(['field1', 'field2'])
|
|
41
|
+
# # - HookContext: context.raise_validation_error('Error message')
|
|
42
|
+
#
|
|
43
|
+
module ForestAdmin
|
|
44
|
+
module Types
|
|
45
|
+
# ============================================
|
|
46
|
+
# Query Components
|
|
47
|
+
# ============================================
|
|
48
|
+
|
|
49
|
+
# Filter - used to filter records in queries
|
|
50
|
+
Filter = ForestAdminDatasourceToolkit::Components::Query::Filter
|
|
51
|
+
|
|
52
|
+
# Aggregation - used for aggregate operations (Count, Sum, Avg, etc.)
|
|
53
|
+
Aggregation = ForestAdminDatasourceToolkit::Components::Query::Aggregation
|
|
54
|
+
|
|
55
|
+
# Projection - specifies which fields to retrieve
|
|
56
|
+
Projection = ForestAdminDatasourceToolkit::Components::Query::Projection
|
|
57
|
+
|
|
58
|
+
# Page - pagination parameters
|
|
59
|
+
Page = ForestAdminDatasourceToolkit::Components::Query::Page
|
|
60
|
+
|
|
61
|
+
# Sort - sorting parameters
|
|
62
|
+
Sort = ForestAdminDatasourceToolkit::Components::Query::Sort
|
|
63
|
+
|
|
64
|
+
# ============================================
|
|
65
|
+
# Condition Tree
|
|
66
|
+
# ============================================
|
|
67
|
+
|
|
68
|
+
# ConditionTreeLeaf - a single condition (field, operator, value)
|
|
69
|
+
ConditionTreeLeaf = ForestAdminDatasourceToolkit::Components::Query::ConditionTree::Nodes::ConditionTreeLeaf
|
|
70
|
+
|
|
71
|
+
# ConditionTreeBranch - combines multiple conditions with And/Or
|
|
72
|
+
ConditionTreeBranch = ForestAdminDatasourceToolkit::Components::Query::ConditionTree::Nodes::ConditionTreeBranch
|
|
73
|
+
|
|
74
|
+
# Operators - all available operators (EQUAL, GREATER_THAN, CONTAINS, etc.)
|
|
75
|
+
Operators = ForestAdminDatasourceToolkit::Components::Query::ConditionTree::Operators
|
|
76
|
+
|
|
77
|
+
# ============================================
|
|
78
|
+
# Action Types
|
|
79
|
+
# ============================================
|
|
80
|
+
|
|
81
|
+
# FieldType - field types for action forms (STRING, NUMBER, BOOLEAN, etc.)
|
|
82
|
+
FieldType = ForestAdminDatasourceCustomizer::Decorators::Action::Types::FieldType
|
|
83
|
+
|
|
84
|
+
# ActionScope - action scopes (SINGLE, BULK, GLOBAL)
|
|
85
|
+
ActionScope = ForestAdminDatasourceCustomizer::Decorators::Action::Types::ActionScope
|
|
86
|
+
|
|
87
|
+
# BaseAction - base class for defining actions
|
|
88
|
+
BaseAction = ForestAdminDatasourceCustomizer::Decorators::Action::BaseAction
|
|
89
|
+
|
|
90
|
+
# ============================================
|
|
91
|
+
# Action Contexts
|
|
92
|
+
# ============================================
|
|
93
|
+
|
|
94
|
+
# ActionContext - execution context for Bulk/Global actions
|
|
95
|
+
ActionContext = ForestAdminDatasourceCustomizer::Decorators::Action::Context::ActionContext
|
|
96
|
+
|
|
97
|
+
# ActionContextSingle - execution context for Single actions
|
|
98
|
+
ActionContextSingle = ForestAdminDatasourceCustomizer::Decorators::Action::Context::ActionContextSingle
|
|
99
|
+
|
|
100
|
+
# ActionResultBuilder - builder for action results (success, error, file, webhook)
|
|
101
|
+
ActionResultBuilder = ForestAdminDatasourceCustomizer::Decorators::Action::ResultBuilder
|
|
102
|
+
|
|
103
|
+
# ============================================
|
|
104
|
+
# Charts
|
|
105
|
+
# ============================================
|
|
106
|
+
|
|
107
|
+
# ChartResultBuilder - builder for chart results (value, distribution, time_based, etc.)
|
|
108
|
+
ChartResultBuilder = ForestAdminDatasourceCustomizer::Decorators::Chart::ResultBuilder
|
|
109
|
+
|
|
110
|
+
# ============================================
|
|
111
|
+
# Computed Fields
|
|
112
|
+
# ============================================
|
|
113
|
+
|
|
114
|
+
# ComputedDefinition - defines computed/virtual fields
|
|
115
|
+
ComputedDefinition = ForestAdminDatasourceCustomizer::Decorators::Computed::ComputedDefinition
|
|
116
|
+
|
|
117
|
+
# ============================================
|
|
118
|
+
# User/Caller Information
|
|
119
|
+
# ============================================
|
|
120
|
+
|
|
121
|
+
# Caller - represents the user making the request (email, timezone, team, etc.)
|
|
122
|
+
Caller = ForestAdminDatasourceToolkit::Components::Caller
|
|
123
|
+
|
|
124
|
+
# ============================================
|
|
125
|
+
# Plugins
|
|
126
|
+
# ============================================
|
|
127
|
+
|
|
128
|
+
# Plugin - base class for creating custom plugins
|
|
129
|
+
Plugin = ForestAdminDatasourceCustomizer::Plugins::Plugin
|
|
130
|
+
|
|
131
|
+
# ActiveStoragePlugin - plugin to expose Active Storage attachments as File fields
|
|
132
|
+
# Lazy-loaded because Zeitwerk may not have autoloaded it yet when types.rb is required
|
|
133
|
+
def self.const_missing(name)
|
|
134
|
+
if name == :ActiveStoragePlugin
|
|
135
|
+
require 'forest_admin_rails/plugins/active_storage'
|
|
136
|
+
const_set(:ActiveStoragePlugin, ForestAdminRails::Plugins::ActiveStorage)
|
|
137
|
+
else
|
|
138
|
+
super
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# ============================================
|
|
143
|
+
# Additional Context Classes
|
|
144
|
+
# ============================================
|
|
145
|
+
|
|
146
|
+
# ChartContext - execution context for chart handlers
|
|
147
|
+
# Methods: get_record(fields), record_id, composite_record_id
|
|
148
|
+
ChartContext = ForestAdminDatasourceCustomizer::Decorators::Chart::ChartContext
|
|
149
|
+
|
|
150
|
+
# HookContext - execution context for hook handlers
|
|
151
|
+
# Methods: raise_validation_error, raise_forbidden_error, raise_error
|
|
152
|
+
HookContext = ForestAdminDatasourceCustomizer::Decorators::Hook::Context::HookContext
|
|
153
|
+
|
|
154
|
+
# ============================================
|
|
155
|
+
# Exception Classes
|
|
156
|
+
# ============================================
|
|
157
|
+
|
|
158
|
+
# BusinessError - base class for all business errors
|
|
159
|
+
BusinessError = ForestAdminAgent::Http::Exceptions::BusinessError
|
|
160
|
+
|
|
161
|
+
# Specific error types for better error handling
|
|
162
|
+
ValidationError = ForestAdminAgent::Http::Exceptions::ValidationError
|
|
163
|
+
ForbiddenError = ForestAdminAgent::Http::Exceptions::ForbiddenError
|
|
164
|
+
UnauthorizedError = ForestAdminAgent::Http::Exceptions::UnauthorizedError
|
|
165
|
+
NotFoundError = ForestAdminAgent::Http::Exceptions::NotFoundError
|
|
166
|
+
UnprocessableError = ForestAdminAgent::Http::Exceptions::UnprocessableError
|
|
167
|
+
BadRequestError = ForestAdminAgent::Http::Exceptions::BadRequestError
|
|
168
|
+
|
|
169
|
+
# ============================================
|
|
170
|
+
# Factory Classes
|
|
171
|
+
# ============================================
|
|
172
|
+
|
|
173
|
+
# ConditionTreeFactory - factory methods for creating condition trees
|
|
174
|
+
# Methods: from_plain_object, match_ids, match_records, intersect, union, match_none
|
|
175
|
+
ConditionTreeFactory = ForestAdminDatasourceToolkit::Components::Query::ConditionTree::ConditionTreeFactory
|
|
176
|
+
|
|
177
|
+
# FilterFactory - factory methods for creating filters
|
|
178
|
+
# Methods: from_plain_object, make_foreign_filter, make_through_filter
|
|
179
|
+
FilterFactory = ForestAdminDatasourceToolkit::Components::Query::FilterFactory
|
|
180
|
+
|
|
181
|
+
# ProjectionFactory - factory methods for creating projections
|
|
182
|
+
# Methods: all(collection)
|
|
183
|
+
ProjectionFactory = ForestAdminDatasourceToolkit::Components::Query::ProjectionFactory
|
|
184
|
+
|
|
185
|
+
# SortFactory - factory methods for creating sort clauses
|
|
186
|
+
# Methods: by_primary_keys(collection)
|
|
187
|
+
SortFactory = ForestAdminDatasourceToolkit::Components::Query::SortUtils::SortFactory
|
|
188
|
+
|
|
189
|
+
# ============================================
|
|
190
|
+
# Module inclusion support
|
|
191
|
+
# ============================================
|
|
192
|
+
|
|
193
|
+
def self.included(base)
|
|
194
|
+
# When included, define constants on the including class/module
|
|
195
|
+
# This allows using `include ForestAdmin::Types` and then referencing
|
|
196
|
+
# the types directly without the ForestAdmin::Types prefix
|
|
197
|
+
constants.each do |const_name|
|
|
198
|
+
base.const_set(const_name, const_get(const_name)) unless base.const_defined?(const_name)
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
@@ -22,35 +22,109 @@ module ForestAdminRails
|
|
|
22
22
|
class Engine < ::Rails::Engine
|
|
23
23
|
isolate_namespace ForestAdminRails
|
|
24
24
|
|
|
25
|
+
extend ActiveSupport::Concern
|
|
26
|
+
|
|
27
|
+
initializer 'forest_admin_rails.error_subscribe' do
|
|
28
|
+
Rails.error.subscribe(ForestAdminErrorSubscriber.new)
|
|
29
|
+
end
|
|
30
|
+
|
|
25
31
|
config.after_initialize do
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
32
|
+
Rails.error.handle(ForestAdminDatasourceToolkit::Exceptions::ForestException, severity: :error) do
|
|
33
|
+
agent_factory = ForestAdminAgent::Builder::AgentFactory.instance
|
|
34
|
+
agent_factory.setup(ForestAdminRails.config)
|
|
35
|
+
load_configuration
|
|
36
|
+
load_cors
|
|
37
|
+
load_correlation_id
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Echo the agent-generated correlation id on every response (mirrors the Node agent's
|
|
42
|
+
# router.use(correlationIdMiddleware)); CORS exposure of the header is handled in load_cors.
|
|
43
|
+
#
|
|
44
|
+
# Ahead of ShowExceptions rather than at the end of the stack: a Rack middleware can only add a header
|
|
45
|
+
# to a response it sees returned, so sitting under the exception handlers meant error responses they
|
|
46
|
+
# build on the way out never carried the id.
|
|
47
|
+
def load_correlation_id
|
|
48
|
+
middleware = ForestAdminAgent::Http::CorrelationIdMiddleware
|
|
49
|
+
|
|
50
|
+
begin
|
|
51
|
+
# The application's stack, not the engine's: ShowExceptions lives there, and an
|
|
52
|
+
# insert_before recorded on the engine's own (empty) proxy raises when it is applied.
|
|
53
|
+
Rails.application.config.middleware.insert_before ActionDispatch::ShowExceptions, middleware
|
|
54
|
+
rescue StandardError
|
|
55
|
+
# No ShowExceptions in this stack (or already gone): the id still reaches every response the app
|
|
56
|
+
# returns normally.
|
|
57
|
+
config.middleware.use middleware
|
|
58
|
+
end
|
|
30
59
|
end
|
|
31
60
|
|
|
32
61
|
def load_configuration
|
|
33
|
-
return unless
|
|
62
|
+
return unless running_web_server?
|
|
63
|
+
return unless create_agent_file_exists?
|
|
34
64
|
|
|
35
65
|
# force eager loading models
|
|
36
66
|
Rails.application.eager_load!
|
|
37
67
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
68
|
+
require create_agent_path.to_s
|
|
69
|
+
|
|
70
|
+
setup_agent_and_cache_routes
|
|
41
71
|
|
|
42
72
|
sse = ForestAdminAgent::Services::SSECacheInvalidation
|
|
43
73
|
sse.run if ForestAdminRails.config[:instant_cache_refresh]
|
|
44
74
|
end
|
|
45
75
|
|
|
76
|
+
def create_agent_path
|
|
77
|
+
Rails.root.join('lib', 'forest_admin_rails', 'create_agent.rb')
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def create_agent_file_exists?
|
|
81
|
+
File.exist?(create_agent_path)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def setup_agent_and_cache_routes
|
|
85
|
+
ForestAdminRails::CreateAgent.setup!
|
|
86
|
+
precache_routes
|
|
87
|
+
rescue StandardError => e
|
|
88
|
+
logger = ActiveSupport::Logger.new($stdout)
|
|
89
|
+
logger.warn 'An error has occurred during setup of the Forest Admin agent.'
|
|
90
|
+
raise e.message
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def precache_routes
|
|
94
|
+
return if ForestAdminAgent::Http::Router.cache_disabled?
|
|
95
|
+
|
|
96
|
+
start_time = Time.now
|
|
97
|
+
routes = ForestAdminAgent::Http::Router.cached_routes
|
|
98
|
+
elapsed = ((Time.now - start_time) * 1000).round(2)
|
|
99
|
+
|
|
100
|
+
logger = ActiveSupport::Logger.new($stdout)
|
|
101
|
+
logger.info("[ForestAdmin] Successfully pre-cached #{routes.size} routes in #{elapsed}ms")
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def running_web_server?
|
|
105
|
+
return true if defined?(::Rails::Server)
|
|
106
|
+
|
|
107
|
+
# check if a web server is running with the given command line arguments
|
|
108
|
+
server_commands = %w[puma unicorn thin passenger rackup]
|
|
109
|
+
return true if server_commands.any? { |cmd| ARGV.any? { |arg| arg.include?(cmd) } }
|
|
110
|
+
|
|
111
|
+
# check if running via common server executables
|
|
112
|
+
return true if $PROGRAM_NAME.match?(/puma|unicorn|thin|passenger/)
|
|
113
|
+
|
|
114
|
+
false
|
|
115
|
+
end
|
|
116
|
+
|
|
46
117
|
def load_cors
|
|
118
|
+
return if ENV['FOREST_CORS_DEACTIVATED']
|
|
119
|
+
|
|
47
120
|
config.middleware.insert_before 0, Rack::Cors do
|
|
48
121
|
allow do
|
|
49
122
|
hostnames = [/\A.*\.forestadmin\.com\z/]
|
|
50
123
|
hostnames += ENV['CORS_ORIGINS'].split(',') if ENV['CORS_ORIGINS']
|
|
51
124
|
|
|
52
125
|
origins hostnames
|
|
53
|
-
resource '*', headers: :any, methods: :any, credentials: true, max_age: 86_400
|
|
126
|
+
resource '*', headers: :any, methods: :any, credentials: true, max_age: 86_400,
|
|
127
|
+
expose: [ForestAdminAgent::Http::CorrelationId::HEADER]
|
|
54
128
|
end
|
|
55
129
|
end
|
|
56
130
|
end
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
require 'base64'
|
|
2
|
+
require 'cgi'
|
|
3
|
+
require 'rack/mime'
|
|
4
|
+
|
|
5
|
+
module ForestAdminRails
|
|
6
|
+
module Plugins
|
|
7
|
+
class ActiveStorage < ForestAdminDatasourceCustomizer::Plugins::Plugin
|
|
8
|
+
ACTIVE_STORAGE_COLLECTIONS = %w[
|
|
9
|
+
ActiveStorage__Attachment
|
|
10
|
+
ActiveStorage__Blob
|
|
11
|
+
ActiveStorage__VariantRecord
|
|
12
|
+
].freeze
|
|
13
|
+
|
|
14
|
+
def run(datasource_customizer, _collection_customizer = nil, options = {})
|
|
15
|
+
datasource_customizer.collections.each do |name, collection|
|
|
16
|
+
next if options[:only] && !options[:only].include?(name)
|
|
17
|
+
next if options[:except]&.include?(name)
|
|
18
|
+
|
|
19
|
+
add_attachment_fields(collection, options)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
remove_active_storage_collections(datasource_customizer) if options.fetch(:hide_internal_collections, true)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def remove_active_storage_collections(datasource_customizer)
|
|
28
|
+
ACTIVE_STORAGE_COLLECTIONS.each do |collection_name|
|
|
29
|
+
next unless datasource_customizer.collections.key?(collection_name)
|
|
30
|
+
|
|
31
|
+
datasource_customizer.remove_collection(collection_name)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def add_attachment_fields(collection, options)
|
|
36
|
+
model_class = find_model_class(collection)
|
|
37
|
+
return unless model_class
|
|
38
|
+
return unless defined?(::ActiveStorage) && model_class.respond_to?(:reflect_on_all_attachments)
|
|
39
|
+
|
|
40
|
+
model_class.reflect_on_all_attachments
|
|
41
|
+
.select { |a| a.macro == :has_one_attached }
|
|
42
|
+
.each do |attachment|
|
|
43
|
+
name = attachment.name.to_s
|
|
44
|
+
hide_attachment_relations(collection, name)
|
|
45
|
+
add_file_field(collection, model_class, name, options.fetch(:download_images_on_list, false))
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def hide_attachment_relations(collection, attachment_name)
|
|
50
|
+
%W[#{attachment_name}_attachment #{attachment_name}_blob].each do |relation_name|
|
|
51
|
+
next unless collection.schema[:fields].key?(relation_name)
|
|
52
|
+
|
|
53
|
+
collection.remove_field(relation_name)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def add_file_field(collection, model_class, attachment_name, download_images_on_list)
|
|
58
|
+
return if collection.schema[:fields][attachment_name]
|
|
59
|
+
|
|
60
|
+
collection.add_field(attachment_name,
|
|
61
|
+
ForestAdminDatasourceCustomizer::Decorators::Computed::ComputedDefinition.new(
|
|
62
|
+
column_type: 'File',
|
|
63
|
+
dependencies: ['id'],
|
|
64
|
+
values: lambda { |records, _ctx|
|
|
65
|
+
compute_values(records, model_class, attachment_name, download_images_on_list)
|
|
66
|
+
}
|
|
67
|
+
))
|
|
68
|
+
|
|
69
|
+
collection.replace_field_writing(attachment_name) do |value, context|
|
|
70
|
+
handle_write(value, context, model_class, attachment_name)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def compute_values(records, model_class, attachment_name, download_images_on_list)
|
|
75
|
+
ids = records.map { |r| r['id'] }
|
|
76
|
+
models = model_class
|
|
77
|
+
.where(id: ids)
|
|
78
|
+
.includes(:"#{attachment_name}_attachment", :"#{attachment_name}_blob")
|
|
79
|
+
.index_by(&:id)
|
|
80
|
+
|
|
81
|
+
records.map do |record|
|
|
82
|
+
model = models[record['id']]
|
|
83
|
+
next nil unless model&.public_send(attachment_name)&.attached?
|
|
84
|
+
|
|
85
|
+
blob = model.public_send(attachment_name).blob
|
|
86
|
+
|
|
87
|
+
if records.length == 1 || (download_images_on_list && blob.content_type&.start_with?('image/'))
|
|
88
|
+
content = blob.download
|
|
89
|
+
"data:#{blob.content_type};name=#{CGI.escape(blob.filename.to_s)};base64,#{Base64.strict_encode64(content)}"
|
|
90
|
+
else
|
|
91
|
+
"data:#{blob.content_type};name=#{CGI.escape(blob.filename.to_s)};base64,"
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def handle_write(value, context, model_class, attachment_name)
|
|
97
|
+
record_id = context.filter&.condition_tree&.value
|
|
98
|
+
return {} unless record_id
|
|
99
|
+
|
|
100
|
+
record = model_class.find(record_id)
|
|
101
|
+
|
|
102
|
+
if value.nil? || value.to_s.strip.empty?
|
|
103
|
+
record.public_send(attachment_name).purge if record.public_send(attachment_name).attached?
|
|
104
|
+
else
|
|
105
|
+
parsed = ForestAdminAgent::Utils::Schema::ForestValueConverter.parse_data_uri(value)
|
|
106
|
+
if parsed
|
|
107
|
+
fallback_extension = Rack::Mime::MIME_TYPES.invert[parsed['mime_type']] || '.bin'
|
|
108
|
+
record.public_send(attachment_name).attach(
|
|
109
|
+
io: StringIO.new(parsed['buffer']),
|
|
110
|
+
filename: parsed['name'] || "#{attachment_name}#{fallback_extension}",
|
|
111
|
+
content_type: parsed['mime_type']
|
|
112
|
+
)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
{}
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def find_model_class(collection)
|
|
119
|
+
current = collection.respond_to?(:collection) ? collection.collection : collection
|
|
120
|
+
loop do
|
|
121
|
+
return current.model if current.respond_to?(:model) && current.model.is_a?(Class)
|
|
122
|
+
break unless current.respond_to?(:child_collection)
|
|
123
|
+
|
|
124
|
+
current = current.child_collection
|
|
125
|
+
end
|
|
126
|
+
nil
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
data/lib/forest_admin_rails.rb
CHANGED
|
@@ -4,9 +4,11 @@ require 'forest_admin_rails/engine'
|
|
|
4
4
|
require 'zeitwerk'
|
|
5
5
|
require 'rails/railtie'
|
|
6
6
|
require 'forest_admin_agent'
|
|
7
|
+
require_relative 'forest_admin/types'
|
|
7
8
|
|
|
8
9
|
loader = Zeitwerk::Loader.for_gem
|
|
9
10
|
loader.ignore("#{__dir__}/generators")
|
|
11
|
+
loader.ignore("#{__dir__}/forest_admin")
|
|
10
12
|
loader.setup
|
|
11
13
|
|
|
12
14
|
module ForestAdminRails
|
|
@@ -15,16 +17,29 @@ module ForestAdminRails
|
|
|
15
17
|
setting :debug, default: true
|
|
16
18
|
setting :auth_secret
|
|
17
19
|
setting :env_secret
|
|
18
|
-
setting :forest_server_url, default: 'https://api.forestadmin.com'
|
|
20
|
+
setting :forest_server_url, default: ENV.fetch('FOREST_SERVER_URL', 'https://api.forestadmin.com')
|
|
19
21
|
setting :is_production, default: Rails.env.production?
|
|
20
|
-
setting :prefix, default:
|
|
22
|
+
setting :prefix, default: nil
|
|
21
23
|
setting :permission_expiration, default: 900
|
|
22
24
|
setting :cache_dir, default: :'tmp/cache/forest_admin'
|
|
23
25
|
setting :schema_path, default: File.join(Dir.pwd, '.forestadmin-schema.json')
|
|
24
26
|
setting :project_dir, default: Dir.pwd
|
|
25
|
-
setting :
|
|
27
|
+
setting :logger_level, default: 'info'
|
|
26
28
|
setting :logger, default: nil
|
|
29
|
+
setting :customize_error_message, default: nil
|
|
27
30
|
setting :instant_cache_refresh, default: Rails.env.production?
|
|
31
|
+
setting :limit_export_size, default: nil
|
|
32
|
+
setting :append_schema_path, default: nil
|
|
33
|
+
setting :skip_schema_update, default: false
|
|
34
|
+
setting :skip_relation_read_permissions, default: false
|
|
35
|
+
setting :disable_route_cache, default: false
|
|
36
|
+
setting :rpc_max_polling_threads, default: nil
|
|
37
|
+
setting :workflow_executor_url, default: nil
|
|
38
|
+
# { database: <ActiveRecord config or URL>, schema:, table_name:, redact: } — setting `database`
|
|
39
|
+
# turns the audit trail on: every change is captured and the `/_audit-trail` routes are registered.
|
|
40
|
+
setting :audit_trail, default: nil
|
|
41
|
+
# Max records a "select all" approval may target; clamped to the Forest server's cap (500).
|
|
42
|
+
setting :max_records_for_approval, default: 500
|
|
28
43
|
|
|
29
44
|
if defined?(Rails::Railtie)
|
|
30
45
|
# logic for cors middleware,... here // or it might be into Engine
|
|
@@ -9,7 +9,7 @@ module ForestAdminRails
|
|
|
9
9
|
@env_secret = env_secret
|
|
10
10
|
template 'initializers/config.rb', 'config/initializers/forest_admin_rails.rb'
|
|
11
11
|
template 'create_agent.rb', 'lib/forest_admin_rails/create_agent.rb'
|
|
12
|
-
route
|
|
12
|
+
route 'mount ForestAdminRails::Engine => "#{ForestAdminRails.config[:prefix]}"'
|
|
13
13
|
end
|
|
14
14
|
end
|
|
15
15
|
end
|
|
@@ -1,4 +1,43 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
namespace :forest_admin do
|
|
2
|
+
namespace :schema do
|
|
3
|
+
desc 'Generate the Forest Admin schema file without starting the server or sending it to the API'
|
|
4
|
+
task generate: :environment do
|
|
5
|
+
require 'forest_admin_agent'
|
|
6
|
+
|
|
7
|
+
debug_mode = ENV['debug'] == 'true'
|
|
8
|
+
|
|
9
|
+
puts '[ForestAdmin] Starting schema generation...'
|
|
10
|
+
|
|
11
|
+
# Force eager loading of all models
|
|
12
|
+
Rails.application.eager_load!
|
|
13
|
+
|
|
14
|
+
# Check if create_agent.rb exists
|
|
15
|
+
create_agent_path = Rails.root.join('lib', 'forest_admin_rails', 'create_agent.rb')
|
|
16
|
+
unless File.exist?(create_agent_path)
|
|
17
|
+
puts '[ForestAdmin] Error: create_agent.rb not found at lib/forest_admin_rails/create_agent.rb'
|
|
18
|
+
puts '[ForestAdmin] Run `rails generate forest_admin_rails <ENV_SECRET>` to create it.'
|
|
19
|
+
exit 1
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Setup the agent factory
|
|
23
|
+
agent_factory = ForestAdminAgent::Builder::AgentFactory.instance
|
|
24
|
+
agent_factory.setup(ForestAdminRails.config)
|
|
25
|
+
|
|
26
|
+
# Enable schema-only mode (generates schema without sending to API)
|
|
27
|
+
agent_factory.schema_only_mode = true
|
|
28
|
+
|
|
29
|
+
# Load and execute the create_agent.rb file
|
|
30
|
+
require create_agent_path
|
|
31
|
+
|
|
32
|
+
# Call setup! which will generate the schema
|
|
33
|
+
begin
|
|
34
|
+
ForestAdminRails::CreateAgent.setup!
|
|
35
|
+
puts '[ForestAdmin] Schema generation completed!'
|
|
36
|
+
rescue StandardError => e
|
|
37
|
+
puts "[ForestAdmin] Error during schema generation: #{e.message}"
|
|
38
|
+
puts e.backtrace.first(10).join("\n") if debug_mode
|
|
39
|
+
exit 1
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
metadata
CHANGED
|
@@ -1,16 +1,86 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: forest_admin_rails
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.44.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Matthieu
|
|
8
8
|
- Nicolas
|
|
9
|
-
autorequire:
|
|
9
|
+
autorequire:
|
|
10
10
|
bindir: bin
|
|
11
11
|
cert_chain: []
|
|
12
|
-
date:
|
|
12
|
+
date: 2026-10-01 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
|
+
- !ruby/object:Gem::Dependency
|
|
15
|
+
name: base64
|
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
|
17
|
+
requirements:
|
|
18
|
+
- - ">="
|
|
19
|
+
- !ruby/object:Gem::Version
|
|
20
|
+
version: '0'
|
|
21
|
+
type: :runtime
|
|
22
|
+
prerelease: false
|
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
24
|
+
requirements:
|
|
25
|
+
- - ">="
|
|
26
|
+
- !ruby/object:Gem::Version
|
|
27
|
+
version: '0'
|
|
28
|
+
- !ruby/object:Gem::Dependency
|
|
29
|
+
name: benchmark
|
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
|
31
|
+
requirements:
|
|
32
|
+
- - ">="
|
|
33
|
+
- !ruby/object:Gem::Version
|
|
34
|
+
version: '0'
|
|
35
|
+
type: :runtime
|
|
36
|
+
prerelease: false
|
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
38
|
+
requirements:
|
|
39
|
+
- - ">="
|
|
40
|
+
- !ruby/object:Gem::Version
|
|
41
|
+
version: '0'
|
|
42
|
+
- !ruby/object:Gem::Dependency
|
|
43
|
+
name: bigdecimal
|
|
44
|
+
requirement: !ruby/object:Gem::Requirement
|
|
45
|
+
requirements:
|
|
46
|
+
- - ">="
|
|
47
|
+
- !ruby/object:Gem::Version
|
|
48
|
+
version: '0'
|
|
49
|
+
type: :runtime
|
|
50
|
+
prerelease: false
|
|
51
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
52
|
+
requirements:
|
|
53
|
+
- - ">="
|
|
54
|
+
- !ruby/object:Gem::Version
|
|
55
|
+
version: '0'
|
|
56
|
+
- !ruby/object:Gem::Dependency
|
|
57
|
+
name: cgi
|
|
58
|
+
requirement: !ruby/object:Gem::Requirement
|
|
59
|
+
requirements:
|
|
60
|
+
- - ">="
|
|
61
|
+
- !ruby/object:Gem::Version
|
|
62
|
+
version: '0'
|
|
63
|
+
type: :runtime
|
|
64
|
+
prerelease: false
|
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
66
|
+
requirements:
|
|
67
|
+
- - ">="
|
|
68
|
+
- !ruby/object:Gem::Version
|
|
69
|
+
version: '0'
|
|
70
|
+
- !ruby/object:Gem::Dependency
|
|
71
|
+
name: csv
|
|
72
|
+
requirement: !ruby/object:Gem::Requirement
|
|
73
|
+
requirements:
|
|
74
|
+
- - ">="
|
|
75
|
+
- !ruby/object:Gem::Version
|
|
76
|
+
version: '0'
|
|
77
|
+
type: :runtime
|
|
78
|
+
prerelease: false
|
|
79
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
80
|
+
requirements:
|
|
81
|
+
- - ">="
|
|
82
|
+
- !ruby/object:Gem::Version
|
|
83
|
+
version: '0'
|
|
14
84
|
- !ruby/object:Gem::Dependency
|
|
15
85
|
name: dry-configurable
|
|
16
86
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -25,6 +95,48 @@ dependencies:
|
|
|
25
95
|
- - "~>"
|
|
26
96
|
- !ruby/object:Gem::Version
|
|
27
97
|
version: '1.1'
|
|
98
|
+
- !ruby/object:Gem::Dependency
|
|
99
|
+
name: logger
|
|
100
|
+
requirement: !ruby/object:Gem::Requirement
|
|
101
|
+
requirements:
|
|
102
|
+
- - ">="
|
|
103
|
+
- !ruby/object:Gem::Version
|
|
104
|
+
version: '0'
|
|
105
|
+
type: :runtime
|
|
106
|
+
prerelease: false
|
|
107
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
108
|
+
requirements:
|
|
109
|
+
- - ">="
|
|
110
|
+
- !ruby/object:Gem::Version
|
|
111
|
+
version: '0'
|
|
112
|
+
- !ruby/object:Gem::Dependency
|
|
113
|
+
name: mutex_m
|
|
114
|
+
requirement: !ruby/object:Gem::Requirement
|
|
115
|
+
requirements:
|
|
116
|
+
- - ">="
|
|
117
|
+
- !ruby/object:Gem::Version
|
|
118
|
+
version: '0'
|
|
119
|
+
type: :runtime
|
|
120
|
+
prerelease: false
|
|
121
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
122
|
+
requirements:
|
|
123
|
+
- - ">="
|
|
124
|
+
- !ruby/object:Gem::Version
|
|
125
|
+
version: '0'
|
|
126
|
+
- !ruby/object:Gem::Dependency
|
|
127
|
+
name: ostruct
|
|
128
|
+
requirement: !ruby/object:Gem::Requirement
|
|
129
|
+
requirements:
|
|
130
|
+
- - ">="
|
|
131
|
+
- !ruby/object:Gem::Version
|
|
132
|
+
version: '0'
|
|
133
|
+
type: :runtime
|
|
134
|
+
prerelease: false
|
|
135
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
136
|
+
requirements:
|
|
137
|
+
- - ">="
|
|
138
|
+
- !ruby/object:Gem::Version
|
|
139
|
+
version: '0'
|
|
28
140
|
- !ruby/object:Gem::Dependency
|
|
29
141
|
name: rails
|
|
30
142
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -64,13 +176,16 @@ extensions: []
|
|
|
64
176
|
extra_rdoc_files: []
|
|
65
177
|
files:
|
|
66
178
|
- LICENSE
|
|
67
|
-
- README.md
|
|
68
179
|
- Rakefile
|
|
69
180
|
- app/controllers/forest_admin_rails/forest_controller.rb
|
|
70
181
|
- app/helpers/forest_admin_rails/application_helper.rb
|
|
182
|
+
- config/initializers/forest_admin_error_subscriber.rb
|
|
183
|
+
- config/initializers/forest_admin_route_cache.rb
|
|
71
184
|
- config/routes.rb
|
|
185
|
+
- lib/forest_admin/types.rb
|
|
72
186
|
- lib/forest_admin_rails.rb
|
|
73
187
|
- lib/forest_admin_rails/engine.rb
|
|
188
|
+
- lib/forest_admin_rails/plugins/active_storage.rb
|
|
74
189
|
- lib/forest_admin_rails/version.rb
|
|
75
190
|
- lib/generators/forest_admin_rails/install_generator.rb
|
|
76
191
|
- lib/generators/forest_admin_rails/templates/create_agent.rb
|
|
@@ -78,13 +193,13 @@ files:
|
|
|
78
193
|
- lib/tasks/forest_admin_rails_tasks.rake
|
|
79
194
|
homepage: https://www.forestadmin.com
|
|
80
195
|
licenses:
|
|
81
|
-
-
|
|
196
|
+
- GPL-3.0
|
|
82
197
|
metadata:
|
|
83
198
|
homepage_uri: https://www.forestadmin.com
|
|
84
|
-
source_code_uri: https://github.com/ForestAdmin/
|
|
85
|
-
changelog_uri: https://github.com/ForestAdmin/
|
|
199
|
+
source_code_uri: https://github.com/ForestAdmin/agent-ruby
|
|
200
|
+
changelog_uri: https://github.com/ForestAdmin/agent-ruby/blob/main/CHANGELOG.md
|
|
86
201
|
rubygems_mfa_required: 'false'
|
|
87
|
-
post_install_message:
|
|
202
|
+
post_install_message:
|
|
88
203
|
rdoc_options: []
|
|
89
204
|
require_paths:
|
|
90
205
|
- lib
|
|
@@ -95,12 +210,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
95
210
|
version: 3.0.0
|
|
96
211
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
97
212
|
requirements:
|
|
98
|
-
- - "
|
|
213
|
+
- - ">="
|
|
99
214
|
- !ruby/object:Gem::Version
|
|
100
|
-
version:
|
|
215
|
+
version: '0'
|
|
101
216
|
requirements: []
|
|
102
|
-
rubygems_version: 3.
|
|
103
|
-
signing_key:
|
|
217
|
+
rubygems_version: 3.4.20
|
|
218
|
+
signing_key:
|
|
104
219
|
specification_version: 4
|
|
105
220
|
summary: Official Rails Agent for Forest Admin.
|
|
106
221
|
test_files: []
|
data/README.md
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
# ForestAdmin
|
|
2
|
-
Short description and motivation.
|
|
3
|
-
|
|
4
|
-
## Usage
|
|
5
|
-
How to use my plugin.
|
|
6
|
-
|
|
7
|
-
## Installation
|
|
8
|
-
Add this line to your application's Gemfile:
|
|
9
|
-
|
|
10
|
-
```ruby
|
|
11
|
-
gem "forest_admin"
|
|
12
|
-
```
|
|
13
|
-
|
|
14
|
-
And then execute:
|
|
15
|
-
```bash
|
|
16
|
-
$ bundle
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
Or install it yourself as:
|
|
20
|
-
```bash
|
|
21
|
-
$ gem install forest_admin
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
## Contributing
|
|
25
|
-
Contribution directions go here.
|
|
26
|
-
|
|
27
|
-
## License
|
|
28
|
-
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|