metabase_query_sync 0.1.1
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/LICENSE.txt +21 -0
- data/README.md +105 -0
- data/bin/metabase-query-sync +5 -0
- data/lib/metabase_query_sync/cli.rb +36 -0
- data/lib/metabase_query_sync/config.rb +12 -0
- data/lib/metabase_query_sync/ir/collection.rb +14 -0
- data/lib/metabase_query_sync/ir/graph.rb +69 -0
- data/lib/metabase_query_sync/ir/model.rb +30 -0
- data/lib/metabase_query_sync/ir/pulse.rb +50 -0
- data/lib/metabase_query_sync/ir/query.rb +22 -0
- data/lib/metabase_query_sync/ir.rb +3 -0
- data/lib/metabase_query_sync/metabase_api/card.rb +33 -0
- data/lib/metabase_query_sync/metabase_api/collection.rb +12 -0
- data/lib/metabase_query_sync/metabase_api/database.rb +8 -0
- data/lib/metabase_query_sync/metabase_api/faraday_metabase_api.rb +118 -0
- data/lib/metabase_query_sync/metabase_api/item.rb +20 -0
- data/lib/metabase_query_sync/metabase_api/model.rb +23 -0
- data/lib/metabase_query_sync/metabase_api/pulse.rb +97 -0
- data/lib/metabase_query_sync/metabase_api/put_card_request.rb +16 -0
- data/lib/metabase_query_sync/metabase_api/put_collection_request.rb +13 -0
- data/lib/metabase_query_sync/metabase_api/put_pulse_request.rb +12 -0
- data/lib/metabase_query_sync/metabase_api/session.rb +7 -0
- data/lib/metabase_query_sync/metabase_api/stub_metabase_api.rb +96 -0
- data/lib/metabase_query_sync/metabase_api.rb +37 -0
- data/lib/metabase_query_sync/metabase_credentials.rb +18 -0
- data/lib/metabase_query_sync/metabase_state.rb +82 -0
- data/lib/metabase_query_sync/read_ir/from_files.rb +20 -0
- data/lib/metabase_query_sync/read_ir.rb +7 -0
- data/lib/metabase_query_sync/sync.rb +167 -0
- data/lib/metabase_query_sync/sync_request.rb +12 -0
- data/lib/metabase_query_sync/types.rb +3 -0
- data/lib/metabase_query_sync/version.rb +3 -0
- data/lib/metabase_query_sync.rb +11 -0
- metadata +176 -0
@@ -0,0 +1,97 @@
|
|
1
|
+
class MetabaseQuerySync::MetabaseApi
|
2
|
+
class Pulse < Model
|
3
|
+
KEY = "pulse"
|
4
|
+
|
5
|
+
class Channel < Model
|
6
|
+
attribute :enabled, MetabaseQuerySync::Types::Strict::Bool.default(true)
|
7
|
+
attribute :schedule_type, MetabaseQuerySync::Types::Strict::String
|
8
|
+
attribute :schedule_day, MetabaseQuerySync::Types::Strict::String.optional
|
9
|
+
attribute :schedule_hour, MetabaseQuerySync::Types::Strict::Integer.optional
|
10
|
+
attribute :schedule_frame, MetabaseQuerySync::Types::Strict::String.optional
|
11
|
+
attribute :channel_type, MetabaseQuerySync::Types::Strict::String.enum('email', 'slack')
|
12
|
+
attribute? :recipients, MetabaseQuerySync::Types::Strict::Array do
|
13
|
+
attribute :email, MetabaseQuerySync::Types::Strict::String
|
14
|
+
end
|
15
|
+
attribute? :details do
|
16
|
+
attribute :channel, MetabaseQuerySync::Types::Strict::String
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.build
|
20
|
+
(yield ChannelBuilder.new).()
|
21
|
+
end
|
22
|
+
|
23
|
+
class ChannelBuilder
|
24
|
+
def initialize
|
25
|
+
@args = {}
|
26
|
+
@class = nil
|
27
|
+
end
|
28
|
+
|
29
|
+
# @param hour [Integer] value between 0 and 23
|
30
|
+
def daily(hour)
|
31
|
+
assert_hour hour
|
32
|
+
@args = @args.merge({schedule_type: 'daily', schedule_day: nil, schedule_frame: nil, schedule_hour: hour})
|
33
|
+
self
|
34
|
+
end
|
35
|
+
|
36
|
+
def assert_hour(hour)
|
37
|
+
raise "invalid hour provided (#{hour})" unless (0..23) === hour
|
38
|
+
end
|
39
|
+
def assert_day(day)
|
40
|
+
raise "invalid day provided (#{day})" unless [:sun, :mon, :tue, :wed, :thu, :fri, :sat] === day
|
41
|
+
end
|
42
|
+
|
43
|
+
# @param hour [Integer] value between 0 and 23
|
44
|
+
# @param day [:sun, :mon, :tue, :wed, :thu, :fri, :sat]
|
45
|
+
def weekly(hour, day)
|
46
|
+
assert_hour hour
|
47
|
+
assert_day day
|
48
|
+
@args = @args.merge({
|
49
|
+
schedule_type: 'weekly',
|
50
|
+
schedule_day: day.to_s,
|
51
|
+
schedule_frame: nil,
|
52
|
+
schedule_hour: hour
|
53
|
+
})
|
54
|
+
self
|
55
|
+
end
|
56
|
+
|
57
|
+
def hourly
|
58
|
+
@args = @args.merge({schedule_type: 'hourly', schedule_day: nil, schedule_frame: nil, schedule_hour: nil})
|
59
|
+
self
|
60
|
+
end
|
61
|
+
|
62
|
+
# @param emails [Array<String>]
|
63
|
+
def emails(emails)
|
64
|
+
@args = @args.merge({channel_type: 'email', recipients: emails.map {|e| {email: e}} })
|
65
|
+
self
|
66
|
+
end
|
67
|
+
|
68
|
+
# @param channel [String]
|
69
|
+
def slack(channel)
|
70
|
+
@args = @args.merge({channel_type: 'slack', details: {channel: channel}})
|
71
|
+
self
|
72
|
+
end
|
73
|
+
|
74
|
+
def call()
|
75
|
+
Channel.new(@args)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
class Card < Model
|
81
|
+
attribute :id, MetabaseQuerySync::Types::Strict::Integer
|
82
|
+
attribute :include_csv, MetabaseQuerySync::Types::Strict::Bool.default(false)
|
83
|
+
attribute :include_xls, MetabaseQuerySync::Types::Strict::Bool.default(false)
|
84
|
+
end
|
85
|
+
|
86
|
+
has :id, :archived, :name, :collection_id
|
87
|
+
attribute :cards, MetabaseQuerySync::Types::Strict::Array.of(Pulse::Card)
|
88
|
+
attribute :channels, MetabaseQuerySync::Types::Strict::Array.of(Pulse::Channel)
|
89
|
+
attribute :skip_if_empty, MetabaseQuerySync::Types::Strict::Bool
|
90
|
+
|
91
|
+
# @param put_pulse_request [PutPulseRequest]
|
92
|
+
# @return self
|
93
|
+
def self.from_request(put_pulse_request)
|
94
|
+
new(put_pulse_request.to_h)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
class MetabaseQuerySync::MetabaseApi
|
2
|
+
class PutCardRequest < Model
|
3
|
+
has :id, :archived, :name, :description, :collection_id
|
4
|
+
attribute :display, MetabaseQuerySync::Types::Strict::String.default('table'.freeze)
|
5
|
+
attribute :visualization_settings, MetabaseQuerySync::Types::Strict::Hash.default({}.freeze)
|
6
|
+
attribute :dataset_query, Card::DatasetQuery
|
7
|
+
|
8
|
+
def self.native(sql:, database_id:, **kwargs)
|
9
|
+
new(dataset_query: Card::DatasetQuery.native(sql: sql, database_id: database_id), **kwargs)
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.from_card(card)
|
13
|
+
new(card.to_h)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class MetabaseQuerySync::MetabaseApi
|
2
|
+
class PutCollectionRequest < Model
|
3
|
+
has :id, :name, :description, :archived
|
4
|
+
attribute :color, MetabaseQuerySync::Types::Strict::String.default('#509EE3'.freeze)
|
5
|
+
attribute :parent_id, MetabaseQuerySync::Types::Strict::Integer.optional.default(nil)
|
6
|
+
|
7
|
+
# TODO: implement collections
|
8
|
+
# # @param collection [Collection]
|
9
|
+
# def self.from_collection(collection)
|
10
|
+
# new(collection.to_h)
|
11
|
+
# end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
class MetabaseQuerySync::MetabaseApi
|
2
|
+
class PutPulseRequest < Model
|
3
|
+
has :id, :name, :archived, :collection_id
|
4
|
+
attribute :cards, MetabaseQuerySync::Types::Strict::Array.of(Pulse::Card)
|
5
|
+
attribute :channels, MetabaseQuerySync::Types::Strict::Array.of(Pulse::Channel)
|
6
|
+
attribute :skip_if_empty, MetabaseQuerySync::Types::Strict::Bool
|
7
|
+
|
8
|
+
def self.from_pulse(pulse)
|
9
|
+
new(pulse.to_h)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'dry-monads'
|
2
|
+
|
3
|
+
class MetabaseQuerySync::MetabaseApi
|
4
|
+
class StubMetabaseApi < self
|
5
|
+
include Dry::Monads[:result]
|
6
|
+
|
7
|
+
attr_reader :requests
|
8
|
+
|
9
|
+
def initialize(collections: [], pulses: [], cards: [], databases: [])
|
10
|
+
@collections = collections
|
11
|
+
@pulses = pulses
|
12
|
+
@cards = cards
|
13
|
+
@databases = databases
|
14
|
+
@requests = []
|
15
|
+
end
|
16
|
+
|
17
|
+
def get_collection(id)
|
18
|
+
find_by_id(@collections, id)
|
19
|
+
end
|
20
|
+
|
21
|
+
def get_card(id)
|
22
|
+
find_by_id(@cards, id)
|
23
|
+
end
|
24
|
+
|
25
|
+
def get_databases
|
26
|
+
Success(@databases)
|
27
|
+
end
|
28
|
+
|
29
|
+
def get_pulse(id)
|
30
|
+
find_by_id(@pulses, id)
|
31
|
+
end
|
32
|
+
|
33
|
+
def put_pulse(pulse_request)
|
34
|
+
put_req(Pulse, pulse_request, @pulses) { |pulses| @pulses = pulses }
|
35
|
+
end
|
36
|
+
|
37
|
+
def put_collection(collection_request)
|
38
|
+
@requests << collection_request
|
39
|
+
Success(collection_request)
|
40
|
+
end
|
41
|
+
|
42
|
+
def put_card(card_request)
|
43
|
+
put_req(Card, card_request, @cards) { |cards| @cards = cards }
|
44
|
+
end
|
45
|
+
|
46
|
+
def get_collection_items(collection_id)
|
47
|
+
get_collection(collection_id).bind do
|
48
|
+
Success(@collections.chain(@pulses, @cards).filter do |item|
|
49
|
+
case item
|
50
|
+
when Collection
|
51
|
+
item.parent_id == collection_id
|
52
|
+
when Pulse
|
53
|
+
item.collection_id == collection_id
|
54
|
+
when Card
|
55
|
+
item.collection_id == collection_id
|
56
|
+
else
|
57
|
+
false
|
58
|
+
end
|
59
|
+
end.map do |item|
|
60
|
+
Item.new(id: item.id, name: item.name, description: item.respond_to?(:description) ? item.description : nil, model: case item
|
61
|
+
when Collection
|
62
|
+
Collection::KEY
|
63
|
+
when Pulse
|
64
|
+
Pulse::KEY
|
65
|
+
when Card
|
66
|
+
Card::KEY
|
67
|
+
end)
|
68
|
+
end)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def find_by_id(items, id)
|
75
|
+
item = items.find { |i| i.id == id }
|
76
|
+
item ? Success(item) : Failure(nil)
|
77
|
+
end
|
78
|
+
|
79
|
+
def match_id(id)
|
80
|
+
->(item) { item.id == id }
|
81
|
+
end
|
82
|
+
|
83
|
+
def put_req(klass, req, collection)
|
84
|
+
@requests << req
|
85
|
+
item = klass.from_request(req)
|
86
|
+
if item.id
|
87
|
+
collection = collection.map { |i| i.id == item.id ? item : i }
|
88
|
+
else
|
89
|
+
item = item.new(id: collection.map { |i| i.id }.compact.max.to_i + 1)
|
90
|
+
collection << item
|
91
|
+
end
|
92
|
+
yield collection
|
93
|
+
Success(item)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# Lightweight metabase api interface to be just enough
|
2
|
+
# for this project.
|
3
|
+
class MetabaseQuerySync::MetabaseApi
|
4
|
+
# collections
|
5
|
+
|
6
|
+
def get_collection(id); throw; end
|
7
|
+
def get_collection_items(collection_id); throw; end
|
8
|
+
# @param collection_request [PutCollectionRequest]
|
9
|
+
def put_collection(collection_request); throw; end
|
10
|
+
|
11
|
+
# cards
|
12
|
+
|
13
|
+
def get_card(id); throw; end
|
14
|
+
# @param [PutCardRequest]
|
15
|
+
def put_card(card_request); throw; end
|
16
|
+
def delete_card(card_id); throw; end
|
17
|
+
|
18
|
+
# pulses
|
19
|
+
|
20
|
+
def get_pulse(id); throw; end
|
21
|
+
# @param [PutPulseRequest]
|
22
|
+
def put_pulse(pulse_request); throw; end
|
23
|
+
def delete_pulse(pulse_id); throw; end
|
24
|
+
|
25
|
+
# database
|
26
|
+
|
27
|
+
def get_databases(); throw; end
|
28
|
+
|
29
|
+
# search
|
30
|
+
def search(q, model: nil); throw; end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def throw
|
35
|
+
raise 'not implemented'
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
class MetabaseQuerySync::MetabaseCredentials
|
2
|
+
attr_reader :host, :user, :pass
|
3
|
+
|
4
|
+
def initialize(host:, user:, pass:)
|
5
|
+
raise "Metabase credentials for host, user, pass must not be empty)" if host == nil || user == nil || pass == nil
|
6
|
+
@host = host
|
7
|
+
@user = user
|
8
|
+
@pass = pass
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.from_env(host: nil, user: nil, pass: nil, env: ENV)
|
12
|
+
self.new(
|
13
|
+
host: host || env['METABASE_QUERY_SYNC_HOST'],
|
14
|
+
user: user || env['METABASE_QUERY_SYNC_USER'],
|
15
|
+
pass: pass || env['METABASE_QUERY_SYNC_PASS'],
|
16
|
+
)
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'dry-struct'
|
2
|
+
|
3
|
+
# Holds all of the data/state that has been previously synced to metabase
|
4
|
+
module MetabaseQuerySync
|
5
|
+
# @!method collections
|
6
|
+
# @return [Array<MetabaseApi::Collection>]
|
7
|
+
# @!method cards
|
8
|
+
# @return [Array<MetabaseApi::Card>]
|
9
|
+
# @!method pulses
|
10
|
+
# @return [Array<MetabaseApi::Pulse>]
|
11
|
+
# @!method databases
|
12
|
+
# @return [Array<MetabaseApi::Database>]
|
13
|
+
class MetabaseState < Dry::Struct
|
14
|
+
attribute :collections, Types::Strict::Array.of(MetabaseApi::Collection)
|
15
|
+
attribute :cards, Types::Strict::Array.of(MetabaseApi::Card)
|
16
|
+
attribute :pulses, Types::Strict::Array.of(MetabaseApi::Pulse)
|
17
|
+
attribute :databases, Types::Strict::Array.of(MetabaseApi::Database)
|
18
|
+
|
19
|
+
# @param metabase_api [MetabaseApi]
|
20
|
+
# @param root_collection_id [Integer]
|
21
|
+
# @return [MetabaseState]
|
22
|
+
def self.from_metabase_api(metabase_api, root_collection_id)
|
23
|
+
items = metabase_api.get_collection_items(root_collection_id)
|
24
|
+
if items.failure?
|
25
|
+
raise "No root collection (id: #{root_collection_id}) found"
|
26
|
+
end
|
27
|
+
|
28
|
+
acc = items.value!
|
29
|
+
.filter { |i| i.card? || i.pulse? }
|
30
|
+
.map do |item|
|
31
|
+
if item.card?
|
32
|
+
metabase_api.get_card(item.id).value!
|
33
|
+
elsif item.pulse?
|
34
|
+
metabase_api.get_pulse(item.id).value!
|
35
|
+
else
|
36
|
+
raise 'Unexpected item type.'
|
37
|
+
end
|
38
|
+
end
|
39
|
+
.reduce({cards: [], pulses: []}) do |acc, item|
|
40
|
+
case item
|
41
|
+
when MetabaseApi::Card
|
42
|
+
acc[:cards] << item
|
43
|
+
when MetabaseApi::Pulse
|
44
|
+
acc[:pulses] << item
|
45
|
+
else
|
46
|
+
raise 'Unexpected item type.'
|
47
|
+
end
|
48
|
+
acc
|
49
|
+
end
|
50
|
+
|
51
|
+
new(collections: [], cards: acc[:cards], pulses: acc[:pulses], databases: metabase_api.get_databases.value!)
|
52
|
+
end
|
53
|
+
|
54
|
+
# @return self
|
55
|
+
def with_card(card)
|
56
|
+
new(cards: cards.concat([card]))
|
57
|
+
end
|
58
|
+
|
59
|
+
# @return self
|
60
|
+
def with_pulse(pulse)
|
61
|
+
new(pulses: pulses.concat([pulse]))
|
62
|
+
end
|
63
|
+
|
64
|
+
def empty?
|
65
|
+
collections.empty? && cards.empty? && pulses.empty?
|
66
|
+
end
|
67
|
+
|
68
|
+
# @return [MetabaseApi::Pulse, nil]
|
69
|
+
def pulse_by_name(name)
|
70
|
+
pulses.filter { |p| p.name.downcase == name.downcase }.first
|
71
|
+
end
|
72
|
+
|
73
|
+
# @return [MetabaseApi::Card, nil]
|
74
|
+
def card_by_name(name)
|
75
|
+
cards.filter { |c| c.name.downcase == name.downcase }.first
|
76
|
+
end
|
77
|
+
|
78
|
+
def database_by_name(name)
|
79
|
+
databases.filter { |d| d.name.downcase == name.downcase }.first
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
class MetabaseQuerySync::ReadIR
|
4
|
+
class FromFiles < self
|
5
|
+
def initialize(path)
|
6
|
+
@path = path
|
7
|
+
end
|
8
|
+
|
9
|
+
def call
|
10
|
+
MetabaseQuerySync::IR::Graph.from_items(
|
11
|
+
# @type [String] f
|
12
|
+
Dir[File.join(@path, "**/*.{query,pulse}.yaml")].map do |f|
|
13
|
+
data = YAML.load_file(f)
|
14
|
+
next MetabaseQuerySync::IR::Query.from_h(data) if f.end_with? 'query.yaml'
|
15
|
+
next MetabaseQuerySync::IR::Pulse.from_h(data) if f.end_with? 'pulse.yaml'
|
16
|
+
end
|
17
|
+
)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,167 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module MetabaseQuerySync
|
4
|
+
# @!attribute metabase_api
|
5
|
+
# @return [MetabaseApi]
|
6
|
+
class Sync
|
7
|
+
def initialize(read_ir, metabase_api, logger = nil)
|
8
|
+
@read_ir = read_ir
|
9
|
+
@metabase_api = metabase_api
|
10
|
+
@logger = logger || Logger.new(IO::NULL)
|
11
|
+
end
|
12
|
+
|
13
|
+
# @param config [Config]
|
14
|
+
def self.from_config(config, logger = nil)
|
15
|
+
new(ReadIR::FromFiles.new(config.path), MetabaseApi::FaradayMetabaseApi.from_metabase_credentials(config.credentials), logger)
|
16
|
+
end
|
17
|
+
|
18
|
+
# @param sync_req [SyncRequest]
|
19
|
+
def call(sync_req)
|
20
|
+
@logger.info "Starting sync with req: #{sync_req.to_h}"
|
21
|
+
graph = @read_ir.()
|
22
|
+
metabase_state = MetabaseState.from_metabase_api(@metabase_api, sync_req.root_collection_id)
|
23
|
+
|
24
|
+
metabase_state = sync_requests(calc_cards_diff(graph, metabase_state, sync_req.root_collection_id), sync_req.dry_run, metabase_state)
|
25
|
+
metabase_state = sync_requests(calc_pulses_diff(graph, metabase_state, sync_req.root_collection_id), sync_req.dry_run, metabase_state)
|
26
|
+
|
27
|
+
@logger.info "Finished sync"
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def calc_cards_diff(graph, metabase_state, root_collection_id)
|
33
|
+
[].chain(
|
34
|
+
delete_cards(graph, metabase_state),
|
35
|
+
add_cards(graph, metabase_state, root_collection_id)
|
36
|
+
)
|
37
|
+
end
|
38
|
+
|
39
|
+
# pulses need to be synced after the cards since pulses need to make use of card ids
|
40
|
+
def calc_pulses_diff(graph, metabase_state, root_collection_id)
|
41
|
+
[].chain(
|
42
|
+
delete_pulses(graph, metabase_state),
|
43
|
+
add_pulses(graph, metabase_state, root_collection_id)
|
44
|
+
)
|
45
|
+
end
|
46
|
+
|
47
|
+
# @param graph [IR::Graph]
|
48
|
+
# @param metabase_state [MetabaseState]
|
49
|
+
def delete_pulses(graph, metabase_state)
|
50
|
+
metabase_state.pulses
|
51
|
+
.filter { |pulse| graph.pulse_by_name(pulse.name) == nil }
|
52
|
+
.map { |pulse| MetabaseApi::PutPulseRequest.from_pulse(pulse).new(archived: true) }
|
53
|
+
end
|
54
|
+
|
55
|
+
# @param graph [IR::Graph]
|
56
|
+
# @param metabase_state [MetabaseState]
|
57
|
+
def delete_cards(graph, metabase_state)
|
58
|
+
metabase_state.cards
|
59
|
+
.filter { |card| graph.query_by_name(card.name) == nil }
|
60
|
+
.map { |card| MetabaseApi::PutCardRequest.from_card(card).new(archived: true) }
|
61
|
+
end
|
62
|
+
|
63
|
+
# @param graph [IR::Graph]
|
64
|
+
# @param metabase_state [MetabaseState]
|
65
|
+
# @param root_collection_id [Integer]
|
66
|
+
def add_cards(graph, metabase_state, root_collection_id)
|
67
|
+
graph.queries
|
68
|
+
.map do |q|
|
69
|
+
[q, metabase_state.card_by_name(q.name)]
|
70
|
+
end
|
71
|
+
.filter do |(q, card)|
|
72
|
+
next true unless card
|
73
|
+
card.dataset_query.native.query != q.sql ||
|
74
|
+
card.database_id != metabase_state.database_by_name(q.database)&.id ||
|
75
|
+
card.description != q.description
|
76
|
+
end
|
77
|
+
.map do |(q, card)|
|
78
|
+
database_id = metabase_state.database_by_name(q.database)&.id
|
79
|
+
raise "Database (#{q.database}) not found" if database_id == nil
|
80
|
+
MetabaseApi::PutCardRequest.native(
|
81
|
+
id: card&.id,
|
82
|
+
sql: q.sql,
|
83
|
+
database_id: metabase_state.database_by_name(q.database)&.id,
|
84
|
+
name: q.name,
|
85
|
+
description: q.description,
|
86
|
+
collection_id: root_collection_id,
|
87
|
+
)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# @param graph [IR::Graph]
|
92
|
+
# @param metabase_state [MetabaseState]
|
93
|
+
# # @param root_collection_id [Integer]
|
94
|
+
def add_pulses(graph, metabase_state, root_collection_id)
|
95
|
+
graph.pulses
|
96
|
+
.map do |pulse|
|
97
|
+
api_pulse = metabase_state.pulse_by_name(pulse.name)
|
98
|
+
pulse_cards = graph
|
99
|
+
.queries_by_pulse(pulse.name)
|
100
|
+
.flat_map do |query|
|
101
|
+
card = metabase_state.card_by_name(query.name)
|
102
|
+
card ? [card] : []
|
103
|
+
end
|
104
|
+
.map { |card| MetabaseApi::Pulse::Card.new(id: card.id) }
|
105
|
+
pulse_channels = pulse.alerts.map do |alert|
|
106
|
+
MetabaseApi::Pulse::Channel.build do |c|
|
107
|
+
case alert.type
|
108
|
+
when 'email'
|
109
|
+
c.emails alert.email.emails
|
110
|
+
when 'slack'
|
111
|
+
c.slack alert.slack.channel
|
112
|
+
end
|
113
|
+
|
114
|
+
case alert.schedule.type
|
115
|
+
when 'hourly'
|
116
|
+
c.hourly
|
117
|
+
when 'daily'
|
118
|
+
c.daily(alert.hour)
|
119
|
+
when 'weekly'
|
120
|
+
c.weekly(alert.hour, alert.day)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
[pulse, api_pulse, pulse_cards, pulse_channels]
|
125
|
+
end
|
126
|
+
.filter do |(pulse, api_pulse, pulse_cards, pulse_channels)|
|
127
|
+
next true unless api_pulse
|
128
|
+
api_pulse.cards != pulse_cards || api_pulse.channels != pulse_channels
|
129
|
+
end
|
130
|
+
.map do |(pulse, api_pulse, pulse_cards, pulse_channels)|
|
131
|
+
MetabaseApi::PutPulseRequest.new(
|
132
|
+
id: api_pulse&.id,
|
133
|
+
name: pulse.name,
|
134
|
+
cards: pulse_cards,
|
135
|
+
channels: pulse_channels,
|
136
|
+
collection_id: root_collection_id,
|
137
|
+
skip_if_empty: true,
|
138
|
+
)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
# sync requests up to metabase
|
143
|
+
# @param requests [Enumerator]
|
144
|
+
# @param metabase_state [MetabaseState]
|
145
|
+
def sync_requests(requests, dry_run, metabase_state)
|
146
|
+
requests.reduce(metabase_state) do |metabase_state, req|
|
147
|
+
case req
|
148
|
+
when MetabaseApi::PutPulseRequest
|
149
|
+
@logger.info "PutPulseRequest #{req.to_h}"
|
150
|
+
next metabase_state if dry_run
|
151
|
+
@metabase_api.put_pulse(req).fmap do |pulse|
|
152
|
+
metabase_state.with_pulse(pulse)
|
153
|
+
end.value!
|
154
|
+
when MetabaseApi::PutCardRequest
|
155
|
+
@logger.info "PutCardRequest #{req.to_h}"
|
156
|
+
next metabase_state if dry_run
|
157
|
+
@metabase_api.put_card(req).fmap do |card|
|
158
|
+
metabase_state.with_card(card)
|
159
|
+
end.value!
|
160
|
+
else
|
161
|
+
@logger.error "Unhandled Request Type: #{req.class}"
|
162
|
+
metabase_state
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'dry-struct'
|
2
|
+
|
3
|
+
module MetabaseQuerySync
|
4
|
+
# @!method root_collection_id
|
5
|
+
# @return [Integer]
|
6
|
+
# @!method dry_run
|
7
|
+
# @return [Boolean]
|
8
|
+
class SyncRequest < Dry::Struct
|
9
|
+
attribute :root_collection_id, Types::Strict::Integer
|
10
|
+
attribute :dry_run, Types::Strict::Bool.default(false)
|
11
|
+
end
|
12
|
+
end
|