metabase_query_sync 0.1.1 → 0.2.0
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 +4 -4
- data/README.md +58 -9
- data/lib/metabase_query_sync/cli.rb +9 -5
- data/lib/metabase_query_sync/config.rb +31 -4
- data/lib/metabase_query_sync/ir/graph.rb +4 -14
- data/lib/metabase_query_sync/ir/pulse.rb +2 -0
- data/lib/metabase_query_sync/ir/query.rb +2 -0
- data/lib/metabase_query_sync/metabase_state.rb +0 -10
- data/lib/metabase_query_sync/read_ir/from_files.rb +38 -8
- data/lib/metabase_query_sync/sync.rb +60 -10
- data/lib/metabase_query_sync/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 196f3dcb53519b8a0dbbdab05d3a237181dc5e381e83e0a9627fe41e0ffa0024
|
4
|
+
data.tar.gz: e0c49e7710a5fbd18cb51ba1749700ed4e74503c6c0d3f63f786383380fbfa56
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e548655a3ad94a845b5966e248ba4376d0b98676fdd2afd13e94cf84b72fb3cd263df7d13147ea3382255df1b6643e92662ba0842996af04edf4cb667820f8bb
|
7
|
+
data.tar.gz: 4fc3a03c4c4e434982d0832d37ad0062c4322a90352636696ddced4f343501d56dcd4a492f97936b74cca5c9a796e88b7ef937e8b921fc7b2e73efb95a7bfc84
|
data/README.md
CHANGED
@@ -31,7 +31,7 @@ Build files with `.query.yaml` or `.pulse.yaml` suffix and sync those files up t
|
|
31
31
|
name: Low Volume Orders
|
32
32
|
sql: 'select * from orders'
|
33
33
|
database: Local DB # must match name of database in metabase
|
34
|
-
pulse:
|
34
|
+
pulse: hourly # must match local pulse id (name of file), throws exception if no pulse is found with that name
|
35
35
|
```
|
36
36
|
|
37
37
|
```yaml
|
@@ -50,48 +50,97 @@ alerts:
|
|
50
50
|
day: mon # first 3 character of day only needed if weekly
|
51
51
|
```
|
52
52
|
|
53
|
+
### Understanding Ids
|
54
|
+
|
55
|
+
Every metabase model defined in a file has an id which defaults to the name of the file minus the query/pulse.yaml suffix. An id can be explicitly set in the file if wanted. All references within files to other files must use the id of the item to reference.
|
56
|
+
|
57
|
+
Given the following folder layout:
|
58
|
+
|
59
|
+
```
|
60
|
+
queries/
|
61
|
+
low-volume.query.yaml
|
62
|
+
catalog/
|
63
|
+
products-missing-images.query.yaml
|
64
|
+
hourly.pulse.yaml
|
65
|
+
```
|
66
|
+
|
67
|
+
The generated ids would be:
|
68
|
+
|
69
|
+
```
|
70
|
+
low-volume
|
71
|
+
catalog/products-missing-images
|
72
|
+
hourly
|
73
|
+
```
|
74
|
+
|
53
75
|
### Running the Sync
|
54
76
|
|
55
77
|
Then using the metabase-query-sync cli tool, you can sync those files directly into metabase:
|
56
78
|
|
57
79
|
```bash
|
58
80
|
Command:
|
59
|
-
metabase-query-sync
|
81
|
+
metabase-query-sync s
|
60
82
|
|
61
83
|
Usage:
|
62
|
-
metabase-query-sync
|
84
|
+
metabase-query-sync s ROOT_COLLECTION_ID [PATHS]
|
63
85
|
|
64
86
|
Description:
|
65
87
|
Sync queries/pulses to your metabase root collection
|
66
88
|
|
67
89
|
Arguments:
|
68
90
|
ROOT_COLLECTION_ID # REQUIRED The root collection id to sync all items under.
|
69
|
-
|
91
|
+
PATHS # The paths to metabase item files to sync from. Support for scoped paths with custom_name:/path/to/folder is supported as well to ensure each imported item is scoped with custom_name.
|
70
92
|
|
71
93
|
Options:
|
72
94
|
--[no-]dry-run, -d # Perform a dry run and do not actually sync to the metabase instance., default: false
|
73
95
|
--host=VALUE, -H VALUE # Metabase Host, if not set, will read from env at METABASE_QUERY_SYNC_HOST
|
74
96
|
--user=VALUE, -u VALUE # Metabase User, if not set, will read from env at METABASE_QUERY_SYNC_USER
|
75
97
|
--pass=VALUE, -p VALUE # Metabase Password, if not set, will read from env at METABASE_QUERY_SYNC_PASS
|
98
|
+
--config-file=VALUE, -f VALUE # explicit path to .metabase-query-sync.erb.yaml file in case its not in the working directory
|
76
99
|
--help, -h # Print this help
|
77
100
|
```
|
78
101
|
|
102
|
+
### Using .metabase-query-sync.erb.yaml
|
103
|
+
|
104
|
+
It's nice to configure the different paths to search for at once instead of configuring into the command each time, and for that, we support setting defalt config in a file named `.metabase-query-sync.yaml` which should be in the same working directory of the command executation.
|
105
|
+
|
106
|
+
Here's an example file:
|
107
|
+
|
108
|
+
```yaml
|
109
|
+
credentials:
|
110
|
+
host: http://metabase:3000
|
111
|
+
user: ragboyjr@icloud.com
|
112
|
+
pass: <%= ENV["METABASE_PASS"] %>
|
113
|
+
paths:
|
114
|
+
- 'sales:app/sales/queries'
|
115
|
+
- 'catalog:app/catalog/queries'
|
116
|
+
```
|
117
|
+
|
118
|
+
### Results in Metabase
|
119
|
+
|
120
|
+
Navigating to metabase under the root collection provided, should show the synced queries and pulses!
|
121
|
+
|
122
|
+

|
123
|
+

|
124
|
+

|
125
|
+
|
79
126
|
## Development
|
80
127
|
|
81
128
|
- Install gems with `bundle install`
|
82
129
|
- Run tests with `bundle exec rspec`
|
83
130
|
|
84
|
-
### TODO
|
85
|
-
|
86
|
-
- Support Collections and Syncing with collections
|
87
|
-
- Matching IR vs MetabaseApi items should go off of the file name + collection id instead of just the name
|
88
131
|
|
89
|
-
|
132
|
+
### Debugging with Metabase
|
90
133
|
|
91
134
|
To setup the local data source for metabase, run `make db`.
|
92
135
|
|
93
136
|
Starting the metabase docker container should automatically initialize an empty metabase installation with the main admin user account (ragboyjr@icloud.com / password123).
|
94
137
|
|
138
|
+
## Roadmap
|
139
|
+
|
140
|
+
- Support syncing from multiple paths with id prefixes
|
141
|
+
- e.g. /path-to-sales-queries:sales /path-to-payment-queries:payment
|
142
|
+
- Support `.metabase-query-sync.erb.yaml` configuration file
|
143
|
+
|
95
144
|
## Contributing
|
96
145
|
|
97
146
|
Bug reports and pull requests are welcome on GitHub at https://github.com/ragboyjr/metabase-query-sync. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/ragboyjr/metabase-query-sync/blob/master/CODE_OF_CONDUCT.md).
|
@@ -14,16 +14,20 @@ module MetabaseQuerySync
|
|
14
14
|
desc 'Sync queries/pulses to your metabase root collection'
|
15
15
|
|
16
16
|
argument :root_collection_id, type: :integer, required: true, desc: 'The root collection id to sync all items under.'
|
17
|
-
argument :
|
17
|
+
argument :paths, type: :array, required: false, desc: 'The paths to metabase item files to sync from. Support for scoped paths with custom_name:/path/to/folder is supported as well to ensure each imported item is scoped with custom_name.'
|
18
18
|
option :dry_run, type: :boolean, default: false, aliases: ['-d'], desc: 'Perform a dry run and do not actually sync to the metabase instance.'
|
19
19
|
option :host, type: :string, aliases: ['-H'], desc: 'Metabase Host, if not set, will read from env at METABASE_QUERY_SYNC_HOST'
|
20
20
|
option :user, type: :string, aliases: ['-u'], desc: 'Metabase User, if not set, will read from env at METABASE_QUERY_SYNC_USER'
|
21
21
|
option :pass, type: :string, aliases: ['-p'], desc: 'Metabase Password, if not set, will read from env at METABASE_QUERY_SYNC_PASS'
|
22
|
+
option :config_file, type: :string, aliases: ['-f'], desc: 'explicit path to .metabase-query-sync.erb.yaml file in case its not in the working directory'
|
22
23
|
|
23
|
-
def call(root_collection_id:,
|
24
|
-
config = MetabaseQuerySync::Config.
|
25
|
-
|
26
|
-
|
24
|
+
def call(root_collection_id:, paths: nil, dry_run: false, host: nil, user: nil, pass: nil, config_file: nil, **)
|
25
|
+
config = MetabaseQuerySync::Config.from_file(
|
26
|
+
config_file || File.join(Dir.pwd, '.metabase-query-sync.erb.yaml'),
|
27
|
+
paths: paths,
|
28
|
+
host: host,
|
29
|
+
user: user,
|
30
|
+
pass: pass,
|
27
31
|
)
|
28
32
|
sync = MetabaseQuerySync::Sync.from_config(config, Logger.new(STDOUT))
|
29
33
|
sync.(MetabaseQuerySync::SyncRequest.new(root_collection_id: root_collection_id.to_i, dry_run: dry_run))
|
@@ -1,12 +1,39 @@
|
|
1
|
+
require 'dry-schema'
|
2
|
+
require 'yaml'
|
3
|
+
require 'erb'
|
4
|
+
|
1
5
|
module MetabaseQuerySync
|
2
6
|
class Config
|
3
|
-
attr_reader :credentials, :
|
7
|
+
attr_reader :credentials, :paths
|
4
8
|
|
5
9
|
# @param credentials [MetabaseQuerySync::MetabaseCredentials]
|
6
|
-
# @param
|
7
|
-
def initialize(credentials:,
|
10
|
+
# @param paths [Array<String>]
|
11
|
+
def initialize(credentials:, paths:)
|
8
12
|
@credentials = credentials
|
9
|
-
@
|
13
|
+
@paths = paths
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.from_file(path, paths: [], host: nil, user: nil, pass: nil)
|
17
|
+
if File.exists? path
|
18
|
+
data = YAML.load(ERB.new(File.read(path)).result)
|
19
|
+
result = Dry::Schema.JSON do
|
20
|
+
required(:paths).value(array[:string], min_size?: 1)
|
21
|
+
required(:credentials).hash do
|
22
|
+
required(:host).filled(:string)
|
23
|
+
required(:user).filled(:string)
|
24
|
+
required(:pass).filled(:string)
|
25
|
+
end
|
26
|
+
end.(data)
|
27
|
+
raise "Invalid data provided in config file: #{result.errors.to_h}" if result.failure?
|
28
|
+
end
|
29
|
+
new(
|
30
|
+
credentials: MetabaseCredentials.new(
|
31
|
+
host: host || data["credentials"]["host"],
|
32
|
+
user: user || data["credentials"]["user"],
|
33
|
+
pass: pass || data["credentials"]["pass"]
|
34
|
+
),
|
35
|
+
paths: paths.empty? ? data["paths"] : paths
|
36
|
+
)
|
10
37
|
end
|
11
38
|
end
|
12
39
|
end
|
@@ -36,27 +36,17 @@ module MetabaseQuerySync::IR
|
|
36
36
|
end)
|
37
37
|
end
|
38
38
|
|
39
|
-
# @return [Query, nil]
|
40
|
-
def query_by_name(name)
|
41
|
-
queries.filter { |query| strcmp(query.name, name) }.first
|
42
|
-
end
|
43
|
-
|
44
|
-
# @return [Pulse, nil]
|
45
|
-
def pulse_by_name(name)
|
46
|
-
pulses.filter { |pulse| strcmp(pulse.name, name) }.first
|
47
|
-
end
|
48
|
-
|
49
39
|
# @return [Array<Query>]
|
50
|
-
def queries_by_pulse(
|
51
|
-
queries.filter { |query|
|
40
|
+
def queries_by_pulse(pulse_id)
|
41
|
+
queries.filter { |query| query.pulse == pulse_id }
|
52
42
|
end
|
53
43
|
|
54
44
|
private
|
55
45
|
|
56
46
|
def assert_traversal
|
57
|
-
|
47
|
+
pulse_ids = pulses.map(&:id).to_set
|
58
48
|
queries.each do |q|
|
59
|
-
raise "No pulse (#{q.pulse}) found for query (#{q.name})" unless
|
49
|
+
raise "No pulse (#{q.pulse}) found for query (#{q.name})" unless pulse_ids === q.pulse
|
60
50
|
end
|
61
51
|
end
|
62
52
|
|
@@ -22,11 +22,13 @@ module MetabaseQuerySync::IR
|
|
22
22
|
attribute :schedule, Schedule
|
23
23
|
end
|
24
24
|
|
25
|
+
attribute :id, string
|
25
26
|
attribute :name, string
|
26
27
|
attribute :skip_if_empty, bool.default(true)
|
27
28
|
attribute :alerts, array.of(Alert)
|
28
29
|
|
29
30
|
validate_with_schema do
|
31
|
+
required(:id).filled(:string)
|
30
32
|
required(:name).filled(:string)
|
31
33
|
optional(:skip_if_empty).value(:bool)
|
32
34
|
required(:alerts).value(:array, min_size?: 1).each do
|
@@ -2,6 +2,7 @@ require 'dry-schema'
|
|
2
2
|
|
3
3
|
module MetabaseQuerySync::IR
|
4
4
|
class Query < Model
|
5
|
+
attribute :id, string
|
5
6
|
attribute :name, string
|
6
7
|
attribute :description, string.optional.default(nil)
|
7
8
|
attribute :sql, string
|
@@ -10,6 +11,7 @@ module MetabaseQuerySync::IR
|
|
10
11
|
attribute :collection, string.optional.default(nil)
|
11
12
|
|
12
13
|
validate_with_schema do
|
14
|
+
required(:id).filled(:string)
|
13
15
|
required(:name).filled(:string)
|
14
16
|
required(:sql).filled(:string)
|
15
17
|
required(:database).filled(:string)
|
@@ -65,16 +65,6 @@ module MetabaseQuerySync
|
|
65
65
|
collections.empty? && cards.empty? && pulses.empty?
|
66
66
|
end
|
67
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
68
|
def database_by_name(name)
|
79
69
|
databases.filter { |d| d.name.downcase == name.downcase }.first
|
80
70
|
end
|
@@ -1,20 +1,50 @@
|
|
1
|
+
require 'logger'
|
1
2
|
require 'yaml'
|
2
3
|
|
3
4
|
class MetabaseQuerySync::ReadIR
|
4
5
|
class FromFiles < self
|
5
|
-
def initialize(path)
|
6
|
-
@
|
6
|
+
def initialize(path, logger = nil)
|
7
|
+
@paths = path.is_a?(Array) ? path : [path]
|
8
|
+
@logger = logger || Logger.new(IO::NULL)
|
9
|
+
|
10
|
+
raise 'Paths must not be empty when reading from files' if @paths.empty?
|
7
11
|
end
|
8
12
|
|
9
13
|
def call
|
10
14
|
MetabaseQuerySync::IR::Graph.from_items(
|
11
|
-
|
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
|
15
|
+
@paths.flat_map { |p| ir_items_from_path(p) }
|
17
16
|
)
|
18
17
|
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
# @param path [String]
|
22
|
+
def ir_items_from_path(path)
|
23
|
+
(scope, path) = split_path(path)
|
24
|
+
@logger.info "Reading IR Items from path (#{path}) and scope (#{scope})"
|
25
|
+
|
26
|
+
# @type [String] f
|
27
|
+
Dir[File.join(path, "**/*.{query,pulse}.yaml")].map do |f|
|
28
|
+
data = YAML.load_file(f)
|
29
|
+
next MetabaseQuerySync::IR::Query.from_h(prefix_id(scope, {"id" => id_from_file(path, f)}.merge(data))) if f.end_with? 'query.yaml'
|
30
|
+
next MetabaseQuerySync::IR::Pulse.from_h(prefix_id(scope, {"id" => id_from_file(path, f)}.merge(data))) if f.end_with? 'pulse.yaml'
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# @param path [String]
|
35
|
+
def split_path(path)
|
36
|
+
path.include?(':') ? path.split(':', 2) : [nil, path]
|
37
|
+
end
|
38
|
+
|
39
|
+
# @param file_path [String]
|
40
|
+
def id_from_file(base_path, file_path)
|
41
|
+
file_path.gsub(/^#{Regexp.quote(File.join(base_path, ''))}(.+)\.(query|pulse)\.yaml$/, '\1')
|
42
|
+
end
|
43
|
+
|
44
|
+
# @param attributes [Hash]
|
45
|
+
def prefix_id(scope, attributes)
|
46
|
+
return attributes unless scope
|
47
|
+
attributes.merge({"id" => File.join(scope, attributes["id"])})
|
48
|
+
end
|
19
49
|
end
|
20
50
|
end
|
@@ -12,7 +12,7 @@ module MetabaseQuerySync
|
|
12
12
|
|
13
13
|
# @param config [Config]
|
14
14
|
def self.from_config(config, logger = nil)
|
15
|
-
new(ReadIR::FromFiles.new(config.
|
15
|
+
new(ReadIR::FromFiles.new(config.paths, logger), MetabaseApi::FaradayMetabaseApi.from_metabase_credentials(config.credentials), logger)
|
16
16
|
end
|
17
17
|
|
18
18
|
# @param sync_req [SyncRequest]
|
@@ -48,7 +48,7 @@ module MetabaseQuerySync
|
|
48
48
|
# @param metabase_state [MetabaseState]
|
49
49
|
def delete_pulses(graph, metabase_state)
|
50
50
|
metabase_state.pulses
|
51
|
-
.filter { |pulse| graph
|
51
|
+
.filter { |pulse| find_graph_pulse(graph, id(pulse)) == nil }
|
52
52
|
.map { |pulse| MetabaseApi::PutPulseRequest.from_pulse(pulse).new(archived: true) }
|
53
53
|
end
|
54
54
|
|
@@ -56,7 +56,7 @@ module MetabaseQuerySync
|
|
56
56
|
# @param metabase_state [MetabaseState]
|
57
57
|
def delete_cards(graph, metabase_state)
|
58
58
|
metabase_state.cards
|
59
|
-
.filter { |card| graph
|
59
|
+
.filter { |card| find_graph_query(graph, id(card)) == nil }
|
60
60
|
.map { |card| MetabaseApi::PutCardRequest.from_card(card).new(archived: true) }
|
61
61
|
end
|
62
62
|
|
@@ -66,11 +66,12 @@ module MetabaseQuerySync
|
|
66
66
|
def add_cards(graph, metabase_state, root_collection_id)
|
67
67
|
graph.queries
|
68
68
|
.map do |q|
|
69
|
-
[q, metabase_state
|
69
|
+
[q, find_api_card(metabase_state, id(q))]
|
70
70
|
end
|
71
71
|
.filter do |(q, card)|
|
72
72
|
next true unless card
|
73
73
|
card.dataset_query.native.query != q.sql ||
|
74
|
+
card.name != api_item_name(q) ||
|
74
75
|
card.database_id != metabase_state.database_by_name(q.database)&.id ||
|
75
76
|
card.description != q.description
|
76
77
|
end
|
@@ -81,7 +82,7 @@ module MetabaseQuerySync
|
|
81
82
|
id: card&.id,
|
82
83
|
sql: q.sql,
|
83
84
|
database_id: metabase_state.database_by_name(q.database)&.id,
|
84
|
-
name: q
|
85
|
+
name: api_item_name(q),
|
85
86
|
description: q.description,
|
86
87
|
collection_id: root_collection_id,
|
87
88
|
)
|
@@ -94,11 +95,11 @@ module MetabaseQuerySync
|
|
94
95
|
def add_pulses(graph, metabase_state, root_collection_id)
|
95
96
|
graph.pulses
|
96
97
|
.map do |pulse|
|
97
|
-
api_pulse = metabase_state
|
98
|
+
api_pulse = find_api_pulse(metabase_state, id(pulse))
|
98
99
|
pulse_cards = graph
|
99
|
-
.queries_by_pulse(pulse.
|
100
|
+
.queries_by_pulse(pulse.id)
|
100
101
|
.flat_map do |query|
|
101
|
-
card = metabase_state
|
102
|
+
card = find_api_card(metabase_state, id(query))
|
102
103
|
card ? [card] : []
|
103
104
|
end
|
104
105
|
.map { |card| MetabaseApi::Pulse::Card.new(id: card.id) }
|
@@ -125,12 +126,14 @@ module MetabaseQuerySync
|
|
125
126
|
end
|
126
127
|
.filter do |(pulse, api_pulse, pulse_cards, pulse_channels)|
|
127
128
|
next true unless api_pulse
|
128
|
-
api_pulse.cards != pulse_cards ||
|
129
|
+
api_pulse.cards != pulse_cards ||
|
130
|
+
api_pulse.channels != pulse_channels ||
|
131
|
+
api_pulse.name != api_item_name(pulse)
|
129
132
|
end
|
130
133
|
.map do |(pulse, api_pulse, pulse_cards, pulse_channels)|
|
131
134
|
MetabaseApi::PutPulseRequest.new(
|
132
135
|
id: api_pulse&.id,
|
133
|
-
name: pulse.name,
|
136
|
+
name: "#{pulse.id}:#{pulse.name}",
|
134
137
|
cards: pulse_cards,
|
135
138
|
channels: pulse_channels,
|
136
139
|
collection_id: root_collection_id,
|
@@ -163,5 +166,52 @@ module MetabaseQuerySync
|
|
163
166
|
end
|
164
167
|
end
|
165
168
|
end
|
169
|
+
|
170
|
+
# @param item [IR::Model]
|
171
|
+
# @return [String]
|
172
|
+
def api_item_name(item)
|
173
|
+
"#{item.id}:#{item.name}"
|
174
|
+
end
|
175
|
+
|
176
|
+
# @param graph [IR::Graph]
|
177
|
+
# @param query_id [String]
|
178
|
+
# @return [IR::Query, nil]
|
179
|
+
def find_graph_query(graph, query_id)
|
180
|
+
graph.queries.filter { |q| id(q) == query_id }.first
|
181
|
+
end
|
182
|
+
|
183
|
+
# @param graph [IR::Graph]
|
184
|
+
# @param pulse_id [String]
|
185
|
+
# @return [IR::Pulse, nil]
|
186
|
+
def find_graph_pulse(graph, pulse_id)
|
187
|
+
graph.pulses.filter { |p| id(p) == pulse_id }.first
|
188
|
+
end
|
189
|
+
|
190
|
+
# @param metabase_state [MetabaseState]
|
191
|
+
# @param card_id [String]
|
192
|
+
# @return [MetabaseApi::Card, nil]
|
193
|
+
def find_api_card(metabase_state, card_id)
|
194
|
+
metabase_state.cards.filter { |c| id(c) == card_id }.first
|
195
|
+
end
|
196
|
+
|
197
|
+
# @param metabase_state [MetabaseState]
|
198
|
+
# @param pulse_id [String]
|
199
|
+
# @return [MetabaseApi::Pulse, nil]
|
200
|
+
def find_api_pulse(metabase_state, pulse_id)
|
201
|
+
metabase_state.pulses.filter { |p| id(p) == pulse_id }.first
|
202
|
+
end
|
203
|
+
|
204
|
+
# gets the normalized id from the given object to be used for comparisons
|
205
|
+
# @return [String]
|
206
|
+
def id(object)
|
207
|
+
case object
|
208
|
+
when IR::Model
|
209
|
+
object.id
|
210
|
+
when MetabaseApi::Model
|
211
|
+
object.name[/^([^:]+):/, 1] # metabase api names are constructed with #{IR id}:#{IR name}
|
212
|
+
else
|
213
|
+
raise "Unexpected object (#{object.class}) provided."
|
214
|
+
end
|
215
|
+
end
|
166
216
|
end
|
167
217
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: metabase_query_sync
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- RJ Garcia
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-02
|
11
|
+
date: 2021-03-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: zeitwerk
|