metabase_query_sync 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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
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,5 @@
1
+ #!/usr/bin/env ruby
2
+ require 'dry/cli'
3
+ require 'metabase_query_sync'
4
+
5
+ Dry::CLI.new(MetabaseQuerySync::CLI).call
@@ -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,3 @@
1
+ # Internal Representation
2
+ module MetabaseQuerySync::IR
3
+ 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,8 @@
1
+ require 'dry-struct'
2
+
3
+ class MetabaseQuerySync::MetabaseApi
4
+ class Database < Model
5
+ attribute :id, MetabaseQuerySync::Types::Strict::Integer
6
+ attribute :name, MetabaseQuerySync::Types::Strict::String
7
+ end
8
+ 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