diff --git a/.rubocop.yml b/.rubocop.yml index 0c999960b..5b0971e65 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -378,7 +378,7 @@ Layout/LineLength: - 'packages/forest_admin_rpc_agent/lib/forest_admin_rpc_agent/routes/sse.rb' RSpec/MultipleMemoizedHelpers: - Max: 10 + Max: 15 Security/Eval: Exclude: diff --git a/packages/forest_admin_datasource_rpc/lib/forest_admin_datasource_rpc/datasource.rb b/packages/forest_admin_datasource_rpc/lib/forest_admin_datasource_rpc/datasource.rb index 53fbb1696..762e6b768 100644 --- a/packages/forest_admin_datasource_rpc/lib/forest_admin_datasource_rpc/datasource.rb +++ b/packages/forest_admin_datasource_rpc/lib/forest_admin_datasource_rpc/datasource.rb @@ -19,6 +19,9 @@ def initialize(options, introspection) @charts = introspection[:charts] @rpc_relations = introspection[:rpc_relations] + native_query_connections = introspection[:nativeQueryConnections] || [] + @live_query_connections = native_query_connections.to_h { |conn| [conn[:name], conn[:name]] } + @schema = { charts: @charts } end @@ -33,5 +36,17 @@ def render_chart(caller, name) client.call_rpc(url, method: :post, payload: { chart: name, caller: caller.to_h }) end + + def execute_native_query(connection_name, query, binds) + client = RpcClient.new(@options[:uri], ForestAdminRpcAgent::Facades::Container.cache(:auth_secret)) + url = 'forest/rpc/native-query' + + ForestAdminRpcAgent::Facades::Container.logger.log( + 'Debug', + "Forwarding native query for connection '#{connection_name}' to the Rpc agent on #{url}." + ) + + client.call_rpc(url, method: :post, payload: { connection_name: connection_name, query: query, binds: binds }) + end end end diff --git a/packages/forest_admin_datasource_rpc/spec/lib/forest_admin_datasource_rpc/datasource_spec.rb b/packages/forest_admin_datasource_rpc/spec/lib/forest_admin_datasource_rpc/datasource_spec.rb index ad44c549b..8b60e3209 100644 --- a/packages/forest_admin_datasource_rpc/spec/lib/forest_admin_datasource_rpc/datasource_spec.rb +++ b/packages/forest_admin_datasource_rpc/spec/lib/forest_admin_datasource_rpc/datasource_spec.rb @@ -27,6 +27,18 @@ module ForestAdminDatasourceRpc it 'add charts' do expect(datasource.schema[:charts]).to include('appointments') end + + it 'stores native query connections' do + datasource_with_connections = described_class.new( + { uri: 'http://localhost' }, + introspection.merge(nativeQueryConnections: [{ name: 'primary' }, { name: 'secondary' }]) + ) + expect(datasource_with_connections.live_query_connections).to eq({ 'primary' => 'primary', 'secondary' => 'secondary' }) + end + + it 'handles missing nativeQueryConnections gracefully' do + expect(datasource.live_query_connections).to eq({}) + end end context 'when call render_chart' do @@ -40,5 +52,20 @@ module ForestAdminDatasourceRpc end end end + + context 'when call execute_native_query' do + it 'forward the call to the server with all parameters' do + query = 'SELECT * FROM users WHERE id = ?' + binds = [1] + + datasource.execute_native_query('primary', query, binds) + + expect(rpc_client).to have_received(:call_rpc) do |url, options| + expect(url).to eq('forest/rpc/native-query') + expect(options[:method]).to eq(:post) + expect(options[:payload]).to eq({ connection_name: 'primary', query: query, binds: binds }) + end + end + end end end diff --git a/packages/forest_admin_rpc_agent/lib/forest_admin_rpc_agent/routes/native_query.rb b/packages/forest_admin_rpc_agent/lib/forest_admin_rpc_agent/routes/native_query.rb new file mode 100644 index 000000000..20ac93058 --- /dev/null +++ b/packages/forest_admin_rpc_agent/lib/forest_admin_rpc_agent/routes/native_query.rb @@ -0,0 +1,22 @@ +require 'jsonapi-serializers' + +module ForestAdminRpcAgent + module Routes + class NativeQuery < BaseRoute + def initialize + super('rpc/native-query', 'post', 'rpc_native_query') + end + + def handle_request(args) + return '{}' unless args[:params]['connection_name'] && args[:params]['query'] + + connection_name = args[:params]['connection_name'] + query = args[:params]['query'] + binds = args[:params]['binds'] || [] + datasource = ForestAdminRpcAgent::Facades::Container.datasource + + datasource.execute_native_query(connection_name, query, binds).to_json + end + end + end +end diff --git a/packages/forest_admin_rpc_agent/lib/forest_admin_rpc_agent/routes/schema.rb b/packages/forest_admin_rpc_agent/lib/forest_admin_rpc_agent/routes/schema.rb index b8ed4c837..d08b829d1 100644 --- a/packages/forest_admin_rpc_agent/lib/forest_admin_rpc_agent/routes/schema.rb +++ b/packages/forest_admin_rpc_agent/lib/forest_admin_rpc_agent/routes/schema.rb @@ -12,10 +12,19 @@ def initialize def handle_request(_params) agent = ForestAdminRpcAgent::Agent.instance schema = agent.customizer.schema - schema[:collections] = agent.customizer.datasource(ForestAdminRpcAgent::Facades::Container.logger) - .collections - .map { |_name, collection| collection.schema.merge({ name: collection.name }) } - .sort_by { |collection| collection[:name] } + datasource = agent.customizer.datasource(ForestAdminRpcAgent::Facades::Container.logger) + + schema[:collections] = datasource.collections + .map { |_name, collection| collection.schema.merge({ name: collection.name }) } + .sort_by { |collection| collection[:name] } + + connections = [] + agent.customizer.datasources.each do |root_datasource| + connections = connections.union( + root_datasource.live_query_connections.keys.map { |connection_name| { name: connection_name } } + ) + end + schema[:nativeQueryConnections] = connections schema.to_json end diff --git a/packages/forest_admin_rpc_agent/spec/lib/forest_admin_rpc_agent/routes/action_form_spec.rb b/packages/forest_admin_rpc_agent/spec/lib/forest_admin_rpc_agent/routes/action_form_spec.rb index 45e21db1f..e574b311c 100644 --- a/packages/forest_admin_rpc_agent/spec/lib/forest_admin_rpc_agent/routes/action_form_spec.rb +++ b/packages/forest_admin_rpc_agent/spec/lib/forest_admin_rpc_agent/routes/action_form_spec.rb @@ -137,7 +137,6 @@ module Routes # rubocop:disable Style/OpenStructUse # rubocop:disable RSpec/VerifiedDoubles - # rubocop:disable RSpec/MultipleMemoizedHelpers describe '#encode_file_element' do context 'when element is of type "File"' do let(:file_element) { OpenStruct.new(type: 'File', value: double('File')) } @@ -165,7 +164,6 @@ module Routes end # rubocop:enable Style/OpenStructUse # rubocop:enable RSpec/VerifiedDoubles - # rubocop:enable RSpec/MultipleMemoizedHelpers end end end diff --git a/packages/forest_admin_rpc_agent/spec/lib/forest_admin_rpc_agent/routes/native_query_spec.rb b/packages/forest_admin_rpc_agent/spec/lib/forest_admin_rpc_agent/routes/native_query_spec.rb new file mode 100644 index 000000000..40be54fc1 --- /dev/null +++ b/packages/forest_admin_rpc_agent/spec/lib/forest_admin_rpc_agent/routes/native_query_spec.rb @@ -0,0 +1,64 @@ +require 'spec_helper' +require 'faraday' + +module ForestAdminRpcAgent + module Routes + include ForestAdminDatasourceRpc + + describe NativeQuery do + let(:route) { described_class.new } + let(:connection_name) { 'primary' } + let(:query) { 'SELECT * FROM users WHERE id = ?' } + let(:binds) { [1] } + let(:params) do + { + 'connection_name' => connection_name, + 'query' => query, + 'binds' => binds + } + end + + let(:query_result) { [{ 'id' => 1, 'name' => 'John Doe' }] } + let(:datasource) { instance_double(ForestAdminDatasourceToolkit::Datasource) } + + before do + allow(ForestAdminRpcAgent::Facades::Container).to receive(:datasource).and_return(datasource) + allow(datasource).to receive(:execute_native_query).and_return(query_result) + end + + describe '#handle_request' do + context 'when connection_name and query are provided' do + it 'executes the native query and returns the result' do + result = route.handle_request(params: params) + expect(result).to eq(query_result.to_json) + expect(datasource).to have_received(:execute_native_query).with(connection_name, query, binds) + end + end + + context 'when binds is not provided' do + it 'defaults to empty array' do + params_without_binds = params.except('binds') + route.handle_request(params: params_without_binds) + expect(datasource).to have_received(:execute_native_query).with(connection_name, query, []) + end + end + + context 'when connection_name is missing' do + it 'returns an empty JSON object' do + result = route.handle_request(params: { 'query' => query }) + expect(result).to eq('{}') + expect(datasource).not_to have_received(:execute_native_query) + end + end + + context 'when query is missing' do + it 'returns an empty JSON object' do + result = route.handle_request(params: { 'connection_name' => connection_name }) + expect(result).to eq('{}') + expect(datasource).not_to have_received(:execute_native_query) + end + end + end + end + end +end diff --git a/packages/forest_admin_rpc_agent/spec/lib/forest_admin_rpc_agent/routes/schema_spec.rb b/packages/forest_admin_rpc_agent/spec/lib/forest_admin_rpc_agent/routes/schema_spec.rb index 50b52cd18..9aae5d34a 100644 --- a/packages/forest_admin_rpc_agent/spec/lib/forest_admin_rpc_agent/routes/schema_spec.rb +++ b/packages/forest_admin_rpc_agent/spec/lib/forest_admin_rpc_agent/routes/schema_spec.rb @@ -14,12 +14,18 @@ module Routes let(:collection_order) { instance_double(Collection, name: 'orders', schema: { fields: ['id', 'total'] }) } let(:collections) { { 'users' => collection_user, 'orders' => collection_order } } let(:schema) { { some_key: 'some_value' } } + let(:datasource_with_connections) do + instance_double(ForestAdminDatasourceToolkit::Datasource, live_query_connections: { 'primary' => 'primary' }) + end let(:expected_schema) do { some_key: 'some_value', collections: [ { fields: ['id', 'total'], name: 'orders' }, { fields: ['id', 'email'], name: 'users' } + ], + nativeQueryConnections: [ + { name: 'primary' } ] }.to_json end @@ -28,7 +34,11 @@ module Routes allow(ForestAdminRpcAgent::Agent).to receive(:instance).and_return(agent) allow(agent).to receive(:customizer).and_return(customizer) allow(ForestAdminRpcAgent::Facades::Container).to receive(:logger).and_return(logger) - allow(customizer).to receive_messages(schema: schema, datasource: datasource) + allow(customizer).to receive_messages( + schema: schema, + datasource: datasource, + datasources: [datasource_with_connections] + ) allow(datasource).to receive(:collections).and_return(collections) end