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
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