metabase_query_sync 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|