From b582d2fed007eaf559797caaaf10dab77b97497d Mon Sep 17 00:00:00 2001 From: jbat Date: Mon, 26 Aug 2024 08:33:42 +1000 Subject: [PATCH] NEP-18440 add create chart endpoint --- lib/superset/chart/create.rb | 40 +++++++++++ lib/superset/chart/duplicate.rb | 75 +++++++++++++++++++++ lib/superset/dataset/list.rb | 2 +- lib/superset/dataset/update_schema.rb | 7 +- spec/superset/chart/create_spec.rb | 31 +++++++++ spec/superset/dataset/list_spec.rb | 4 +- spec/superset/dataset/update_schema_spec.rb | 12 +++- 7 files changed, 164 insertions(+), 7 deletions(-) create mode 100644 lib/superset/chart/create.rb create mode 100644 lib/superset/chart/duplicate.rb create mode 100644 spec/superset/chart/create_spec.rb diff --git a/lib/superset/chart/create.rb b/lib/superset/chart/create.rb new file mode 100644 index 0000000..a09e8fb --- /dev/null +++ b/lib/superset/chart/create.rb @@ -0,0 +1,40 @@ +=begin +Create a new chart from a set of params +Suggestion is to base your params of an existing charts params and then modify them as needed +So .. why not call the Superset::Chart::Duplicate class which then calls this Chart::Create class + +This class is a bit more generic and can be used to create a new chart from scratch (if your confident in the params) + +Usage: +Superset::Chart::Create.new(params: new_chart_params).perform +=end + +module Superset + module Chart + class Create < Superset::Request + + attr_reader :params + + def initialize(params: ) + @params = params + end + + def perform + raise "Error: params hash is required" unless params.present? && params.is_a?(Hash) + + logger.info("Creating New Chart") + response['id'] + end + + def response + @response ||= client.post(route, params) + end + + private + + def route + "chart/" + end + end + end +end diff --git a/lib/superset/chart/duplicate.rb b/lib/superset/chart/duplicate.rb new file mode 100644 index 0000000..674123c --- /dev/null +++ b/lib/superset/chart/duplicate.rb @@ -0,0 +1,75 @@ +# There is no API endpoint to duplicate charts in Superset. +# This class is a workaround. +# Requires a source chart id, target dataset id + +module Superset + module Chart + class Duplicate < Superset::Request + + attr_reader :source_chart_id, :target_dataset_id, :new_chart_name + + def initialize(source_chart_id: , target_dataset_id: , new_chart_name: ) + @source_chart_id = source_chart_id + @target_dataset_id = target_dataset_id + @new_chart_name = new_chart_name + end + + def perform + raise "Error: source_chart_id integer is required" unless source_chart_id.present? && source_chart_id.is_a?(Integer) + raise "Error: target_dataset_id integer is required" unless target_dataset_id.present? && target_dataset_id.is_a?(Integer) + raise "Error: new_chart_name string is required" unless new_chart_name.present? && new_chart_name.is_a?(String) + + logger.info("Duplicating Chart #{source_chart_id}:#{source_chart['slice_name']}. New chart dataset #{target_dataset_id} and new chart name #{new_chart_name}") + Superset::Chart::Create.new(params: new_chart_params).perform + end + + private + + def new_chart_params + # pulled list from Swagger GUI for chart POST request + # commented out params seem to be not required .. figured out by trial and error + { + #"cache_timeout": 0, + #"certification_details": "string", + #"certified_by": "string", + #"dashboards": [ 0 ], + "datasource_id": target_dataset_id, + # "datasource_name": new_chart_name, + "datasource_type": "table", + # "description": "", + # "external_url": "string", + # "is_managed_externally": true, + # "owners": [ 3 ], # TODO .. check if this is a Required attr, might need to get current API users id. + "params": new_chart_internal_params, + "query_context": new_chart_internal_query_context, + "query_context_generation": true, + "slice_name": new_chart_name, + "viz_type": source_chart['viz_type'] + } + end + + def new_chart_internal_params + new_params = JSON.parse(source_chart['params']) + new_params['datasource'] = new_params['datasource'].gsub(source_chart_dataset_id.to_s, target_dataset_id.to_s) + new_params.delete('slice_id') # refers to the source chart id .. a new id will be generated in the new chart + new_params.to_json + end + + def new_chart_internal_query_context + new_query_context = JSON.parse(source_chart['query_context']) + new_query_context['datasource'] = new_query_context['datasource']['id'] = target_dataset_id + new_query_context['form_data']['datasource'] = new_query_context['form_data']['datasource'].gsub(source_chart_dataset_id.to_s, target_dataset_id.to_s) + new_query_context['form_data'].delete('slice_id') + new_query_context.to_json + end + + def source_chart + @source_chart ||= Superset::Chart::Get.new(source_chart_id).result[0] + end + + def source_chart_dataset_id + @source_chart_dataset_id ||= JSON.parse(source_chart[:query_context])['datasource']['id'] + end + end + end +end diff --git a/lib/superset/dataset/list.rb b/lib/superset/dataset/list.rb index 12f2afd..cb48f3f 100644 --- a/lib/superset/dataset/list.rb +++ b/lib/superset/dataset/list.rb @@ -34,7 +34,7 @@ def filters end def list_attributes - ['id', 'table_name', 'schema', 'changed_by_name'] + ['id', 'table_name', 'database', 'schema', 'changed_by_name'] end end end diff --git a/lib/superset/dataset/update_schema.rb b/lib/superset/dataset/update_schema.rb index 41a1575..10092e3 100644 --- a/lib/superset/dataset/update_schema.rb +++ b/lib/superset/dataset/update_schema.rb @@ -68,9 +68,10 @@ def source_dataset def validate_proposed_changes logger.info " Validating Dataset ID: #{source_dataset_id} schema update to #{target_schema} on Database: #{target_database_id}" - raise "Error: source_dataset_id integer is required" unless source_dataset_id.present? && source_dataset_id.is_a?(Integer) - raise "Error: target_database_id integer is required" unless target_database_id.present? && target_database_id.is_a?(Integer) - raise "Error: target_schema string is required" unless target_schema.present? && target_schema.is_a?(String) + raise "Error: source_dataset_id integer is required" unless source_dataset_id.present? && source_dataset_id.is_a?(Integer) + raise "Error: target_database_id integer is required" unless target_database_id.present? && target_database_id.is_a?(Integer) + raise "Error: target_schema string is required" unless target_schema.present? && target_schema.is_a?(String) + raise "Error: schema must be set on the source dataset" unless source_dataset['schema'].present? # required for validating sql_query_includes_hard_coded_schema # confirm the dataset exist? ... no need as the load_source_dataset method will raise an error if the dataset does not exist diff --git a/spec/superset/chart/create_spec.rb b/spec/superset/chart/create_spec.rb new file mode 100644 index 0000000..db09100 --- /dev/null +++ b/spec/superset/chart/create_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper' +require 'superset/chart/create' + +RSpec.describe Superset::Chart::Create do + subject { described_class.new(params: params ) } + let(:params) { { title: 'a chart' } } + let(:new_chart_id) { 101 } + + let(:response) do + { 'id' => new_chart_id } + end + + before do + allow_any_instance_of(described_class).to receive(:response).and_return(response) + allow(ENV).to receive(:[]).with(any_args) { ''} + end + + describe '#perform' do + it 'returns id of the new chart' do + expect(subject.perform).to eq(new_chart_id) + end + + context 'raises an error if params are not provided' do + let(:params) { nil } + + specify do + expect { subject.perform }.to raise_error("Error: params hash is required") + end + end + end +end diff --git a/spec/superset/dataset/list_spec.rb b/spec/superset/dataset/list_spec.rb index e103daa..992b30c 100644 --- a/spec/superset/dataset/list_spec.rb +++ b/spec/superset/dataset/list_spec.rb @@ -55,8 +55,8 @@ specify do expect(subject.rows).to eq( [ - ["2", "birth_names", "public", "bob"], - ["3", "birth_days", "public", "bob"] + ["2", "birth_names", "{\"database_name\"=>\"examples\", \"id\"=>1}", "public", "bob"], + ["3", "birth_days", "{\"database_name\"=>\"examples\", \"id\"=>1}", "public", "bob"] ] ) end diff --git a/spec/superset/dataset/update_schema_spec.rb b/spec/superset/dataset/update_schema_spec.rb index 5290ea7..9e439ae 100644 --- a/spec/superset/dataset/update_schema_spec.rb +++ b/spec/superset/dataset/update_schema_spec.rb @@ -8,6 +8,7 @@ remove_copy_suffix: remove_copy_suffix) } let(:source_dataset_id) { 226 } + let(:source_schema) { 'schema_one' } let(:target_database_id) { 6 } let(:target_schema) { 'schema_three' } let(:remove_copy_suffix) { false } @@ -59,7 +60,7 @@ "normalize_columns"=>false, "offset"=>0, "owners"=>[], - "schema"=>"schema_one", + "schema"=>source_schema, "sql"=>"select count(*),\nservice\n\nfrom blahblah \ngroup by service", "table_name"=>"JR SP Service Counts (COPY)", "template_params"=>nil @@ -115,6 +116,15 @@ expect { subject.perform }.to raise_error(RuntimeError, "Error: Schema schema_four does not exist in database: 6") end end + + context 'source dataset schema is not configured' do + let(:source_schema) { '' } + + specify do + expect { subject.perform }.to raise_error(RuntimeError, "Error: schema must be set on the source dataset") + end + end + end end