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.
Files changed (35) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +105 -0
  4. data/bin/metabase-query-sync +5 -0
  5. data/lib/metabase_query_sync/cli.rb +36 -0
  6. data/lib/metabase_query_sync/config.rb +12 -0
  7. data/lib/metabase_query_sync/ir/collection.rb +14 -0
  8. data/lib/metabase_query_sync/ir/graph.rb +69 -0
  9. data/lib/metabase_query_sync/ir/model.rb +30 -0
  10. data/lib/metabase_query_sync/ir/pulse.rb +50 -0
  11. data/lib/metabase_query_sync/ir/query.rb +22 -0
  12. data/lib/metabase_query_sync/ir.rb +3 -0
  13. data/lib/metabase_query_sync/metabase_api/card.rb +33 -0
  14. data/lib/metabase_query_sync/metabase_api/collection.rb +12 -0
  15. data/lib/metabase_query_sync/metabase_api/database.rb +8 -0
  16. data/lib/metabase_query_sync/metabase_api/faraday_metabase_api.rb +118 -0
  17. data/lib/metabase_query_sync/metabase_api/item.rb +20 -0
  18. data/lib/metabase_query_sync/metabase_api/model.rb +23 -0
  19. data/lib/metabase_query_sync/metabase_api/pulse.rb +97 -0
  20. data/lib/metabase_query_sync/metabase_api/put_card_request.rb +16 -0
  21. data/lib/metabase_query_sync/metabase_api/put_collection_request.rb +13 -0
  22. data/lib/metabase_query_sync/metabase_api/put_pulse_request.rb +12 -0
  23. data/lib/metabase_query_sync/metabase_api/session.rb +7 -0
  24. data/lib/metabase_query_sync/metabase_api/stub_metabase_api.rb +96 -0
  25. data/lib/metabase_query_sync/metabase_api.rb +37 -0
  26. data/lib/metabase_query_sync/metabase_credentials.rb +18 -0
  27. data/lib/metabase_query_sync/metabase_state.rb +82 -0
  28. data/lib/metabase_query_sync/read_ir/from_files.rb +20 -0
  29. data/lib/metabase_query_sync/read_ir.rb +7 -0
  30. data/lib/metabase_query_sync/sync.rb +167 -0
  31. data/lib/metabase_query_sync/sync_request.rb +12 -0
  32. data/lib/metabase_query_sync/types.rb +3 -0
  33. data/lib/metabase_query_sync/version.rb +3 -0
  34. data/lib/metabase_query_sync.rb +11 -0
  35. 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,7 @@
1
+ class MetabaseQuerySync::MetabaseApi
2
+ # @!method id
3
+ # @return [String]
4
+ class Session < Model
5
+ attribute :id, MetabaseQuerySync::Types::Strict::String
6
+ end
7
+ 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,7 @@
1
+ # ReadIR from source and load up instances of an IR root model
2
+ class MetabaseQuerySync::ReadIR
3
+ # @return [MetabaseQuerySync::IR::Graph]
4
+ def call()
5
+ raise 'not implemented.'
6
+ end
7
+ 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
@@ -0,0 +1,3 @@
1
+ module MetabaseQuerySync::Types
2
+ include Dry.Types
3
+ end
@@ -0,0 +1,3 @@
1
+ module MetabaseQuerySync
2
+ VERSION = '0.1.1'
3
+ end
@@ -0,0 +1,11 @@
1
+ require 'zeitwerk'
2
+
3
+ loader = Zeitwerk::Loader.for_gem
4
+ loader.inflector.inflect("ir" => "IR")
5
+ loader.inflector.inflect("read_ir" => "ReadIR")
6
+ loader.inflector.inflect("cli" => "CLI")
7
+ loader.setup
8
+
9
+ module MetabaseQuerySync
10
+
11
+ end