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
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 020bfbb2b8958b1f6f3ed8067628bfb4942ddc8a751abc787b797c562b1500d8
|
4
|
+
data.tar.gz: ed21dd175d50d34dec40ae0fac3c29b65da668cba7d80c23bcb93d4b527d0a0d
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6b41d731113fea1a55c85875b762a777d5ce77a27dc6efe9f5088ce22febbbc5a5beccc4cf12260a782a6b4e67fddcc7961941d3be1b747fee5ab747c5ca753a
|
7
|
+
data.tar.gz: 6b646e18d35d39a5da020fa43b3f0d9dbc9e9782fcb440a98371960f8866e659b266ad559f7710acc12de0bc21c9fb7e6176e0fcd09a6cc35c102be5c6ca0634
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2021 TODO: Write your name
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
# MetabaseQuerySync
|
2
|
+
|
3
|
+
MetabaseQuerySync is a tool for automatically syncing metabase queries defined in files to a specific metabase installation.
|
4
|
+
|
5
|
+
This enables metabase queries to be maintained with the relevant source code to ease refactoring of models in your application.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
gem 'metabase_query_sync'
|
13
|
+
```
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
$ bundle install
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
|
21
|
+
$ gem install metabase-query-sync
|
22
|
+
|
23
|
+
## Usage
|
24
|
+
|
25
|
+
Build files with `.query.yaml` or `.pulse.yaml` suffix and sync those files up to your metabase instance.
|
26
|
+
|
27
|
+
### Files Definitions
|
28
|
+
|
29
|
+
```yaml
|
30
|
+
# in low-volume-orders.query.yaml
|
31
|
+
name: Low Volume Orders
|
32
|
+
sql: 'select * from orders'
|
33
|
+
database: Local DB # must match name of database in metabase
|
34
|
+
pulse: Hourly # must match local pulse name field, throws exception if no pulse is found with that name
|
35
|
+
```
|
36
|
+
|
37
|
+
```yaml
|
38
|
+
# in hourly.pulse.yaml
|
39
|
+
name: Hourly
|
40
|
+
alerts:
|
41
|
+
- type: email # can be one of slack/email
|
42
|
+
email:
|
43
|
+
emails: ['ragboyjr@icloud.com']
|
44
|
+
# or instead
|
45
|
+
#slack:
|
46
|
+
# channel: '#test-channel'
|
47
|
+
schedule:
|
48
|
+
type: hourly # can be one of hourly, daily, weekly
|
49
|
+
hour: 5 # number from 0-23, only needed if daily or weekly
|
50
|
+
day: mon # first 3 character of day only needed if weekly
|
51
|
+
```
|
52
|
+
|
53
|
+
### Running the Sync
|
54
|
+
|
55
|
+
Then using the metabase-query-sync cli tool, you can sync those files directly into metabase:
|
56
|
+
|
57
|
+
```bash
|
58
|
+
Command:
|
59
|
+
metabase-query-sync sync
|
60
|
+
|
61
|
+
Usage:
|
62
|
+
metabase-query-sync sync ROOT_COLLECTION_ID PATH
|
63
|
+
|
64
|
+
Description:
|
65
|
+
Sync queries/pulses to your metabase root collection
|
66
|
+
|
67
|
+
Arguments:
|
68
|
+
ROOT_COLLECTION_ID # REQUIRED The root collection id to sync all items under.
|
69
|
+
PATH # REQUIRED The path to metabase item files to sync from.
|
70
|
+
|
71
|
+
Options:
|
72
|
+
--[no-]dry-run, -d # Perform a dry run and do not actually sync to the metabase instance., default: false
|
73
|
+
--host=VALUE, -H VALUE # Metabase Host, if not set, will read from env at METABASE_QUERY_SYNC_HOST
|
74
|
+
--user=VALUE, -u VALUE # Metabase User, if not set, will read from env at METABASE_QUERY_SYNC_USER
|
75
|
+
--pass=VALUE, -p VALUE # Metabase Password, if not set, will read from env at METABASE_QUERY_SYNC_PASS
|
76
|
+
--help, -h # Print this help
|
77
|
+
```
|
78
|
+
|
79
|
+
## Development
|
80
|
+
|
81
|
+
- Install gems with `bundle install`
|
82
|
+
- Run tests with `bundle exec rspec`
|
83
|
+
|
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
|
+
|
89
|
+
## Debugging with Metabase
|
90
|
+
|
91
|
+
To setup the local data source for metabase, run `make db`.
|
92
|
+
|
93
|
+
Starting the metabase docker container should automatically initialize an empty metabase installation with the main admin user account (ragboyjr@icloud.com / password123).
|
94
|
+
|
95
|
+
## Contributing
|
96
|
+
|
97
|
+
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).
|
98
|
+
|
99
|
+
## License
|
100
|
+
|
101
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
102
|
+
|
103
|
+
## Code of Conduct
|
104
|
+
|
105
|
+
Everyone interacting in the Metabase::Query::Sync project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/ragboyjr/metabase-query-sync/blob/master/CODE_OF_CONDUCT.md).
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'dry/cli'
|
2
|
+
|
3
|
+
module MetabaseQuerySync
|
4
|
+
class CLI
|
5
|
+
extend Dry::CLI::Registry
|
6
|
+
|
7
|
+
class Version < Dry::CLI::Command
|
8
|
+
def call
|
9
|
+
puts VERSION
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class Sync < Dry::CLI::Command
|
14
|
+
desc 'Sync queries/pulses to your metabase root collection'
|
15
|
+
|
16
|
+
argument :root_collection_id, type: :integer, required: true, desc: 'The root collection id to sync all items under.'
|
17
|
+
argument :path, type: :string, required: true, desc: 'The path to metabase item files to sync from.'
|
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
|
+
option :host, type: :string, aliases: ['-H'], desc: 'Metabase Host, if not set, will read from env at METABASE_QUERY_SYNC_HOST'
|
20
|
+
option :user, type: :string, aliases: ['-u'], desc: 'Metabase User, if not set, will read from env at METABASE_QUERY_SYNC_USER'
|
21
|
+
option :pass, type: :string, aliases: ['-p'], desc: 'Metabase Password, if not set, will read from env at METABASE_QUERY_SYNC_PASS'
|
22
|
+
|
23
|
+
def call(root_collection_id:, path:, dry_run: false, host: nil, user: nil, pass: nil)
|
24
|
+
config = MetabaseQuerySync::Config.new(
|
25
|
+
credentials: MetabaseQuerySync::MetabaseCredentials.from_env(host: host, user: user, pass: pass),
|
26
|
+
path: path,
|
27
|
+
)
|
28
|
+
sync = MetabaseQuerySync::Sync.from_config(config, Logger.new(STDOUT))
|
29
|
+
sync.(MetabaseQuerySync::SyncRequest.new(root_collection_id: root_collection_id.to_i, dry_run: dry_run))
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
register "version", Version, aliases: ['v', '-v', '--version']
|
34
|
+
register "sync", Sync, aliases: ['s']
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module MetabaseQuerySync
|
2
|
+
class Config
|
3
|
+
attr_reader :credentials, :path
|
4
|
+
|
5
|
+
# @param credentials [MetabaseQuerySync::MetabaseCredentials]
|
6
|
+
# @param path [String]
|
7
|
+
def initialize(credentials:, path:)
|
8
|
+
@credentials = credentials
|
9
|
+
@path = path
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module MetabaseQuerySync::IR
|
2
|
+
class Collection < Model
|
3
|
+
attribute :name, string
|
4
|
+
attribute :description, string.optional
|
5
|
+
attribute :collection, string.optional
|
6
|
+
|
7
|
+
# TODO: implement collections
|
8
|
+
# validate_with_schema do
|
9
|
+
# required(:name).filled(:string)
|
10
|
+
# optional(:description).filled(:string)
|
11
|
+
# optional(:collection).filled(:string)
|
12
|
+
# end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'dry-struct'
|
2
|
+
require 'set'
|
3
|
+
|
4
|
+
module MetabaseQuerySync::IR
|
5
|
+
# @!method collections
|
6
|
+
# @return [Array<Collection>]
|
7
|
+
# @!method queries
|
8
|
+
# @return [Array<Card>]
|
9
|
+
# @!method pulses
|
10
|
+
# @return [Array<Pulse>]
|
11
|
+
class Graph < Dry::Struct
|
12
|
+
attribute :collections, MetabaseQuerySync::Types::Strict::Array.of(Collection)
|
13
|
+
attribute :pulses, MetabaseQuerySync::Types::Strict::Array.of(Pulse)
|
14
|
+
attribute :queries, MetabaseQuerySync::Types::Strict::Array.of(Query)
|
15
|
+
|
16
|
+
def initialize(attributes)
|
17
|
+
super(attributes)
|
18
|
+
assert_traversal
|
19
|
+
end
|
20
|
+
|
21
|
+
# create a struct from a heterogeneous collection of collection, pulse, or queries
|
22
|
+
# @return [Graph]
|
23
|
+
def self.from_items(items)
|
24
|
+
new(items.reduce({collections: [], pulses: [], queries: []}) do |acc, item|
|
25
|
+
case item
|
26
|
+
when Collection
|
27
|
+
acc[:collections] << item
|
28
|
+
when Pulse
|
29
|
+
acc[:pulses] << item
|
30
|
+
when Query
|
31
|
+
acc[:queries] << item
|
32
|
+
else
|
33
|
+
raise "Unexpected type provided from items (#{item.class}), expected instances of only Collection, Pulse, or Query"
|
34
|
+
end
|
35
|
+
acc
|
36
|
+
end)
|
37
|
+
end
|
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
|
+
# @return [Array<Query>]
|
50
|
+
def queries_by_pulse(pulse_name)
|
51
|
+
queries.filter { |query| strcmp(query.pulse, pulse_name) }
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def assert_traversal
|
57
|
+
pulse_names = pulses.map(&:name).map(&:downcase).to_set
|
58
|
+
queries.each do |q|
|
59
|
+
raise "No pulse (#{q.pulse}) found for query (#{q.name})" unless pulse_names === q.pulse.downcase
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# @param a [String]
|
64
|
+
# @param b [String]
|
65
|
+
def strcmp(a, b)
|
66
|
+
a.downcase == b.downcase
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'dry-struct'
|
2
|
+
require 'dry-schema'
|
3
|
+
|
4
|
+
class MetabaseQuerySync::IR::Model < Dry::Struct
|
5
|
+
transform_keys &:to_sym
|
6
|
+
|
7
|
+
def self.string
|
8
|
+
MetabaseQuerySync::Types::Strict::String
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.integer
|
12
|
+
MetabaseQuerySync::Types::Strict::Integer
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.bool
|
16
|
+
MetabaseQuerySync::Types::Strict::Bool
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.array
|
20
|
+
MetabaseQuerySync::Types::Strict::Array
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.validate_with_schema(&schema_def)
|
24
|
+
define_singleton_method :from_h do |h|
|
25
|
+
result = Dry::Schema.JSON(&schema_def).(h)
|
26
|
+
raise "Invalid hash provided: #{result.errors.to_h}" if result.failure?
|
27
|
+
new(h)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module MetabaseQuerySync::IR
|
2
|
+
class Pulse < Model
|
3
|
+
class Alert < Model
|
4
|
+
TYPES = ['email', 'slack'].freeze
|
5
|
+
class Email < Model
|
6
|
+
attribute :emails, array.of(string)
|
7
|
+
end
|
8
|
+
class Slack < Model
|
9
|
+
attribute :channel, string
|
10
|
+
end
|
11
|
+
class Schedule < Model
|
12
|
+
TYPES = ['hourly', 'daily', 'weekly'].freeze
|
13
|
+
DAYS = [nil, 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'].freeze
|
14
|
+
attribute :type, string.enum(*TYPES)
|
15
|
+
attribute :hour, integer.optional.default(nil)
|
16
|
+
attribute :day, string.optional.default(nil).enum(*DAYS)
|
17
|
+
end
|
18
|
+
|
19
|
+
attribute :type, string.enum(*TYPES)
|
20
|
+
attribute :email, Email.optional.default(nil)
|
21
|
+
attribute :slack, Slack.optional.default(nil)
|
22
|
+
attribute :schedule, Schedule
|
23
|
+
end
|
24
|
+
|
25
|
+
attribute :name, string
|
26
|
+
attribute :skip_if_empty, bool.default(true)
|
27
|
+
attribute :alerts, array.of(Alert)
|
28
|
+
|
29
|
+
validate_with_schema do
|
30
|
+
required(:name).filled(:string)
|
31
|
+
optional(:skip_if_empty).value(:bool)
|
32
|
+
required(:alerts).value(:array, min_size?: 1).each do
|
33
|
+
hash do
|
34
|
+
required(:type).value(:filled?, :str?, included_in?: Alert::TYPES)
|
35
|
+
required(:schedule).hash do
|
36
|
+
required(:type).value(:filled?, :str?, included_in?: Alert::Schedule::TYPES)
|
37
|
+
optional(:hour).value(:integer)
|
38
|
+
optional(:day).value(included_in?: Alert::Schedule::DAYS)
|
39
|
+
end
|
40
|
+
optional(:email).hash do
|
41
|
+
required(:emails).value(array[:string], min_size?: 1)
|
42
|
+
end
|
43
|
+
optional(:slack).hash do
|
44
|
+
required(:channel).filled(:string)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'dry-schema'
|
2
|
+
|
3
|
+
module MetabaseQuerySync::IR
|
4
|
+
class Query < Model
|
5
|
+
attribute :name, string
|
6
|
+
attribute :description, string.optional.default(nil)
|
7
|
+
attribute :sql, string
|
8
|
+
attribute :database, string
|
9
|
+
attribute :pulse, string
|
10
|
+
attribute :collection, string.optional.default(nil)
|
11
|
+
|
12
|
+
validate_with_schema do
|
13
|
+
required(:name).filled(:string)
|
14
|
+
required(:sql).filled(:string)
|
15
|
+
required(:database).filled(:string)
|
16
|
+
required(:pulse).filled(:string)
|
17
|
+
required(:pulse).filled(:string)
|
18
|
+
optional(:description).filled(:string)
|
19
|
+
optional(:collection).filled(:string)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
class MetabaseQuerySync::MetabaseApi
|
2
|
+
class Card < Model
|
3
|
+
KEY = "card"
|
4
|
+
|
5
|
+
class DatasetQuery < Model
|
6
|
+
attribute :type, MetabaseQuerySync::Types::Strict::String.default('native'.freeze)
|
7
|
+
attribute :native do
|
8
|
+
attribute :query, MetabaseQuerySync::Types::Strict::String
|
9
|
+
end
|
10
|
+
attribute :database, MetabaseQuerySync::Types::Strict::Integer
|
11
|
+
|
12
|
+
def self.native(sql:, database_id:)
|
13
|
+
new(type: 'native', native: {query: sql}, database: database_id)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
has :id, :archived, :name, :description, :collection_id
|
18
|
+
attribute :database_id, MetabaseQuerySync::Types::Strict::Integer
|
19
|
+
attribute :query_type, MetabaseQuerySync::Types::Strict::String.default('native'.freeze)
|
20
|
+
attribute :display, MetabaseQuerySync::Types::Strict::String.default('table'.freeze)
|
21
|
+
attribute :visualization_settings, MetabaseQuerySync::Types::Strict::Hash.default({}.freeze)
|
22
|
+
attribute :dataset_query, DatasetQuery
|
23
|
+
|
24
|
+
def self.native(database_id:,sql:,**kwargs)
|
25
|
+
new(database_id: database_id, dataset_query: DatasetQuery.native(sql: sql, database_id: database_id), **kwargs)
|
26
|
+
end
|
27
|
+
|
28
|
+
# @param put_card_request [PutCardRequest]
|
29
|
+
def self.from_request(put_card_request)
|
30
|
+
new(put_card_request.to_h.merge(database_id: put_card_request.dataset_query.database))
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'dry-struct'
|
2
|
+
|
3
|
+
class MetabaseQuerySync::MetabaseApi
|
4
|
+
class Collection < Model
|
5
|
+
KEY = 'collection'
|
6
|
+
|
7
|
+
has :id, :archived, :name, :description
|
8
|
+
attribute :slug, MetabaseQuerySync::Types::Strict::String
|
9
|
+
attribute :location, MetabaseQuerySync::Types::Strict::String
|
10
|
+
attribute :parent_id, MetabaseQuerySync::Types::Strict::Integer.optional
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
require 'faraday'
|
2
|
+
require 'faraday_middleware'
|
3
|
+
require 'dry-monads'
|
4
|
+
|
5
|
+
class MetabaseQuerySync::MetabaseApi
|
6
|
+
class FaradayMetabaseApi < self
|
7
|
+
include Dry::Monads[:result]
|
8
|
+
|
9
|
+
# @param client [Faraday::Connection]
|
10
|
+
# @param user [String]
|
11
|
+
# @param pass [String]
|
12
|
+
def initialize(client:, user:, pass:)
|
13
|
+
@client = client
|
14
|
+
@user = user
|
15
|
+
@pass = pass
|
16
|
+
end
|
17
|
+
|
18
|
+
# @param creds [MetabaseQuerySync::MetabaseCredentials]
|
19
|
+
def self.from_metabase_credentials(creds, &configure_faraday)
|
20
|
+
client = Faraday.new(url: creds.host) do |c|
|
21
|
+
c.request :json, content_type: /\bjson$/
|
22
|
+
c.response :json, content_type: /\bjson$/
|
23
|
+
c.request :url_encoded, content_type: /x-www-form-urlencoded/
|
24
|
+
# c.response :logger
|
25
|
+
c.adapter Faraday.default_adapter
|
26
|
+
c.headers['User-Agent'] =
|
27
|
+
"MetabaseQuerySync/#{MetabaseQuerySync::VERSION} (#{RUBY_ENGINE}#{RUBY_VERSION})"
|
28
|
+
|
29
|
+
configure_faraday.call(c) if configure_faraday
|
30
|
+
end
|
31
|
+
|
32
|
+
new(client: client, user: creds.user, pass: creds.pass)
|
33
|
+
end
|
34
|
+
|
35
|
+
def search(q, model: nil)
|
36
|
+
request(:get, '/api/search') do |req|
|
37
|
+
req.params.update(q: q, model: model)
|
38
|
+
end.fmap to_collection_of(Item)
|
39
|
+
end
|
40
|
+
|
41
|
+
def get_collection(id)
|
42
|
+
request(:get, "/api/collection/#{id}").fmap to(Collection)
|
43
|
+
end
|
44
|
+
|
45
|
+
def get_collection_items(collection_id)
|
46
|
+
request(:get, "/api/collection/#{collection_id}/items").fmap to_collection_of(Item)
|
47
|
+
end
|
48
|
+
|
49
|
+
def put_collection(collection_request)
|
50
|
+
if collection_request.id
|
51
|
+
request(:put, "/api/collection/#{collection_request.id}", body: collection_request.to_h)
|
52
|
+
else
|
53
|
+
request(:post, "/api/collection", body: collection_request.to_h)
|
54
|
+
end.fmap to(Collection)
|
55
|
+
end
|
56
|
+
|
57
|
+
def get_databases
|
58
|
+
request(:get, '/api/database').fmap to_collection_of(Database)
|
59
|
+
end
|
60
|
+
|
61
|
+
def get_card(id)
|
62
|
+
request(:get, "/api/card/#{id}").fmap to(Card)
|
63
|
+
end
|
64
|
+
|
65
|
+
def put_card(card_request)
|
66
|
+
if card_request.id
|
67
|
+
request(:put, "/api/card/#{card_request.id}", body: card_request.to_h)
|
68
|
+
else
|
69
|
+
request(:post, "/api/card", body: card_request.to_h)
|
70
|
+
end.fmap to(Card)
|
71
|
+
end
|
72
|
+
|
73
|
+
def get_pulse(id)
|
74
|
+
request(:get, "/api/pulse/#{id}").fmap to(Pulse)
|
75
|
+
end
|
76
|
+
|
77
|
+
def put_pulse(pulse_request)
|
78
|
+
if pulse_request.id
|
79
|
+
request(:put, "/api/pulse/#{pulse_request.id}", body: pulse_request.to_h)
|
80
|
+
else
|
81
|
+
request(:post, "/api/pulse", body: pulse_request.to_h)
|
82
|
+
end.fmap to(Pulse)
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
def to(klass)
|
88
|
+
klass.method(:new)
|
89
|
+
end
|
90
|
+
|
91
|
+
def to_collection_of(klass)
|
92
|
+
->(collection) { collection.map(&klass.method(:new)) }
|
93
|
+
end
|
94
|
+
|
95
|
+
def token
|
96
|
+
@token ||= login.value!.id
|
97
|
+
end
|
98
|
+
|
99
|
+
def login
|
100
|
+
return request(:post, '/api/session', body: {
|
101
|
+
username: @user,
|
102
|
+
password: @pass
|
103
|
+
}, skip_token: true).fmap &to(Session)
|
104
|
+
end
|
105
|
+
|
106
|
+
def request(method, path, body: nil, headers: nil, skip_token: false, &block)
|
107
|
+
res = @client.run_request(method, path, body, headers) do |req|
|
108
|
+
req.headers['X-Metabase-Session'] = token unless skip_token
|
109
|
+
block.call(req) if block
|
110
|
+
end
|
111
|
+
if (200..299) === res.status
|
112
|
+
Success(res.body)
|
113
|
+
else
|
114
|
+
Failure(res)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'dry-struct'
|
2
|
+
|
3
|
+
class MetabaseQuerySync::MetabaseApi
|
4
|
+
class Item < Model
|
5
|
+
attribute :id, MetabaseQuerySync::Types::Strict::Integer
|
6
|
+
attribute :name, MetabaseQuerySync::Types::Strict::String
|
7
|
+
attribute :description, MetabaseQuerySync::Types::Strict::String.optional.default(nil)
|
8
|
+
attribute :model, MetabaseQuerySync::Types::Strict::String
|
9
|
+
|
10
|
+
def card?
|
11
|
+
model == Card::KEY
|
12
|
+
end
|
13
|
+
def collection?
|
14
|
+
model == Collection::KEY
|
15
|
+
end
|
16
|
+
def pulse?
|
17
|
+
model == Pulse::KEY
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'dry-struct'
|
2
|
+
class MetabaseQuerySync::MetabaseApi::Model < Dry::Struct
|
3
|
+
transform_keys &:to_sym
|
4
|
+
|
5
|
+
def self.has(*args)
|
6
|
+
args.each do |sym|
|
7
|
+
case sym
|
8
|
+
when :id
|
9
|
+
attribute :id, MetabaseQuerySync::Types::Strict::Integer.optional.default(nil)
|
10
|
+
when :archived
|
11
|
+
attribute :archived, MetabaseQuerySync::Types::Strict::Bool.default(false)
|
12
|
+
when :name
|
13
|
+
attribute :name, MetabaseQuerySync::Types::Strict::String
|
14
|
+
when :description
|
15
|
+
attribute :description, MetabaseQuerySync::Types::Strict::String.optional.default(nil)
|
16
|
+
when :collection_id
|
17
|
+
attribute :collection_id, MetabaseQuerySync::Types::Strict::Integer.optional.default(nil)
|
18
|
+
else
|
19
|
+
raise "Unexpected field for model (#{sym})"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|