Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,18 @@ module Action
class BaseAction
attr_reader :scope, :form, :is_generate_file, :description, :submit_button_label, :execute, :static_form

def initialize(scope:, form: nil, is_generate_file: false, description: nil, submit_button_label: nil, &execute)
# rubocop:disable Metrics/ParameterLists
def initialize(scope:, form: nil, is_generate_file: false, description: nil,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function with many parameters (count = 7): initialize [qlty:function-parameters]

submit_button_label: nil, static_form: false, &execute)
@scope = scope
@form = form
@is_generate_file = is_generate_file
@description = description
@submit_button_label = submit_button_label
@execute = execute
@static_form = false
@static_form = static_form
end
# rubocop:enable Metrics/ParameterLists

def self.from_plain_object(action)
new(
Expand All @@ -21,6 +24,7 @@ def self.from_plain_object(action)
is_generate_file: action[:is_generate_file],
description: action[:description],
submit_button_label: action[:submit_button_label],
static_form: action[:static_form],
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WDYT action[:static_form] || false to avoid @static_form = nil when the key is missing ?

&action[:execute]
)
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,42 @@ module ForestAdminDatasourceRpc
it 'uses the datasource shared RPC client' do
expect(collection.instance_variable_get(:@client)).to eq(datasource.shared_rpc_client)
end

context 'when the schema carries action static_form values' do
let(:actions_introspection) do
{
charts: [],
rpc_relations: [],
collections: [
{
name: 'Files',
countable: false,
searchable: false,
charts: [],
segments: [],
fields: {
id: {
column_type: 'Number', filter_operators: [], is_primary_key: true,
is_read_only: false, is_sortable: true, default_value: nil,
enum_values: [], validation: [], type: 'Column'
}
},
actions: {
static_action: { scope: 'global', static_form: true },
dynamic_action: { scope: 'global', static_form: false }
}
}
]
}
end
let(:actions_datasource) { Datasource.new({ uri: 'http://localhost' }, actions_introspection) }

it 'preserves :static_form from the wire instead of recomputing it against an empty form' do
schema = actions_datasource.get_collection('Files').schema[:actions]
expect(schema['static_action'].static_form).to be(true)
expect(schema['dynamic_action'].static_form).to be(false)
end
end
end

context 'when call list' do
Expand Down
76 changes: 51 additions & 25 deletions packages/forest_admin_rpc_agent/lib/forest_admin_rpc_agent/agent.rb
Original file line number Diff line number Diff line change
Expand Up @@ -84,32 +84,9 @@ def build_rpc_schema_from_datasource(datasource)
relations = {}

if @rpc_collections.include?(collection.name)
# RPC collection → extract relations to non-RPC collections
collection.schema[:fields].each do |field_name, field|
next if field.type == 'Column'
next if relation_targets_rpc_collection?(field)

relations[field_name] = field
end
extract_rpc_collection_relations(collection, relations)
else
fields = {}

collection.schema[:fields].each do |field_name, field|
if field.type != 'Column' && relation_targets_rpc_collection?(field)
relations[field_name] = field
else
if field.type == 'Column'
field.filter_operators = ForestAdminAgent::Utils::Schema::FrontendFilterable.sort_operators(
field.filter_operators
)
end

fields[field_name] = field
end
end

# Normal collection → include in schema
collections << collection.schema.merge({ name: collection.name, fields: fields })
collections << build_normal_collection_payload(collection, relations)
end

rpc_relations[collection.name] = relations unless relations.empty?
Expand All @@ -131,6 +108,55 @@ def build_rpc_schema_from_datasource(datasource)
schema
end

# RPC collection → extract relations targeting non-RPC collections.
def extract_rpc_collection_relations(collection, relations)
collection.schema[:fields].each do |field_name, field|
next if field.type == 'Column'
next if relation_targets_rpc_collection?(field)

relations[field_name] = field
end
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function with high complexity (count = 6): extract_rpc_collection_relations [qlty:function-complexity]

end

# Normal (non-RPC) collection → split fields between local schema and cross-RPC relations.
def build_normal_collection_payload(collection, relations)
fields = {}

collection.schema[:fields].each do |field_name, field|
if field.type != 'Column' && relation_targets_rpc_collection?(field)
relations[field_name] = field
else
if field.type == 'Column'
field.filter_operators = ForestAdminAgent::Utils::Schema::FrontendFilterable.sort_operators(
field.filter_operators
)
end
fields[field_name] = field
end
end

collection.schema.merge(
name: collection.name,
fields: fields,
actions: serialize_actions(collection.schema[:actions])
)
end

# Only expose the fields needed by an RPC consumer: scope, is_generate_file, static_form,
# description, submit_button_label. Drops `form` (computed via /action-form) and `execute`
# (server-side callback) — neither belongs on the wire.
def serialize_actions(actions)
(actions || {}).transform_values do |action|
{
scope: action.scope,
is_generate_file: action.is_generate_file,
static_form: action.static_form,
description: action.description,
submit_button_label: action.submit_button_label
}
end
end

def relation_targets_rpc_collection?(relation)
if relation.type == 'PolymorphicManyToOne'
relation.foreign_collections.any? { |fc| @rpc_collections.include?(fc) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,10 +128,52 @@ module ForestAdminRpcAgent

parsed = JSON.parse(written_content, symbolize_names: true)
# RPC schema format includes collections with full schemas and native_query_connections
expect(parsed[:collections]).to eq([{ fields: {}, name: 'Test' }])
expect(parsed[:collections]).to eq([{ fields: {}, name: 'Test', actions: {} }])
expect(parsed[:native_query_connections]).to eq([{ name: 'main' }])
end

it 'exposes only the RPC-relevant action fields on the wire' do
allow(ForestAdminRpcAgent::Facades::Container).to receive(:cache) do |key|
{ skip_schema_update: false, schema_path: '/tmp/test-schema.json', is_production: false }[key]
end

action = instance_double(
ForestAdminDatasourceCustomizer::Decorators::Action::BaseAction,
scope: 'Global',
is_generate_file: true,
static_form: false,
description: 'Download a report',
submit_button_label: 'Go',
form: [{ id: 'should_not_leak' }],
execute: -> {}
)
test_collection = instance_double(ForestAdminDatasourceToolkit::Collection)
allow(test_collection).to receive_messages(
name: 'Files',
schema: { fields: {}, actions: { 'download' => action } }
)
allow(datasource).to receive_messages(
collections: { 'Files' => test_collection },
live_query_connections: {}
)

written_content = nil
allow(File).to receive(:write) { |_path, content| written_content = content }

instance.send_schema

parsed = JSON.parse(written_content, symbolize_names: true)
expect(parsed[:collections][0][:actions]).to eq(
download: {
scope: 'Global',
is_generate_file: true,
static_form: false,
description: 'Download a report',
submit_button_label: 'Go'
}
)
end

it 'caches the schema with an etag' do
allow(ForestAdminRpcAgent::Facades::Container).to receive(:cache) do |key|
{ skip_schema_update: false, schema_path: '/tmp/test-schema.json', is_production: false }[key]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ module Routes
end
let(:args) { { params: params } }
let(:expected_response) { { success: true }.to_json }
let(:response) { instance_double(Faraday::Response, success?: true, body: expected_response, status: 200) }
let(:response) do
instance_double(Faraday::Response, success?: true, body: expected_response, status: 200,
headers: {})
end
let(:faraday_connection) { instance_double(Faraday::Connection) }

before do
Expand Down
Loading