diff --git a/plugins/extractors/metabase/README.md b/plugins/extractors/metabase/README.md index 608f0758a..a5c74e88b 100644 --- a/plugins/extractors/metabase/README.md +++ b/plugins/extractors/metabase/README.md @@ -7,8 +7,9 @@ source: type: metabase config: host: http://localhost:3000 - user_id: meteor_tester + username: meteor_tester password: meteor_pass_1234 + label: my-metabase ``` ## Inputs @@ -16,8 +17,9 @@ source: | Key | Value | Example | Description | | | :-- | :---- | :------ | :---------- | :- | | `host` | `string` | `http://localhost:4002` | The host at which metabase is running | *required* | -| `user_id` | `string` | `meteor_tester` | User ID to access the metabase| *required* | +| `username` | `string` | `meteor_tester` | Username/email to access the metabase| *required* | | `password` | `string` | `meteor_pass_1234` | Password for the metabase | *required* | +| `label` | `string` | `meteor_pass_1234` | Label for your Metabase instance, this will be used as part of dashboard's URN | *required* | ## Outputs diff --git a/plugins/extractors/metabase/main_test.go b/plugins/extractors/metabase/main_test.go new file mode 100644 index 000000000..6f62864dd --- /dev/null +++ b/plugins/extractors/metabase/main_test.go @@ -0,0 +1,240 @@ +package metabase_test + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + "os" + "testing" + "time" + + "github.com/odpf/meteor/plugins/extractors/metabase" + "github.com/odpf/meteor/test/utils" + "github.com/ory/dockertest/v3" + "github.com/ory/dockertest/v3/docker" +) + +var session_id string +var ( + port = "4002" + host = "http://localhost:" + port + email = "user@example.com" + pass = "meteor_pass_1234" + populatedDashboards = []metabase.Dashboard{} + dashboardCards = map[int][]metabase.Card{} +) + +func TestMain(m *testing.M) { + // setup test + opts := dockertest.RunOptions{ + Repository: "metabase/metabase", + Tag: "latest", + ExposedPorts: []string{port, "3000"}, + PortBindings: map[docker.Port][]docker.PortBinding{ + "3000": { + {HostIP: "0.0.0.0", HostPort: port}, + }, + }, + } + + retryFn := func(resource *dockertest.Resource) (err error) { + res, err := http.Get(host + "/api/health") + if err != nil { + return + } + if res.StatusCode != http.StatusOK { + return fmt.Errorf("received %d status code", res.StatusCode) + } + return + } + + // Exponential backoff-retry for container to be resy to accept connections + purgeFn, err := utils.CreateContainer(opts, retryFn) + if err != nil { + log.Fatal(err) + } + if err := setup(); err != nil { + log.Fatal(err) + } + + // Run tests + code := m.Run() + + // Clean tests + if err := purgeFn(); err != nil { + log.Fatal(err) + } + os.Exit(code) +} + +func setup() (err error) { + type responseToken struct { + Token string `json:"setup-token"` + } + var data responseToken + err = makeRequest("GET", host+"/api/session/properties", nil, &data) + if err != nil { + return + } + err = setSessionID(data.Token) + if err != nil { + return + } + err = addMockData() + if err != nil { + return + } + return +} + +func addMockData() (err error) { + collection_id, err := addCollection() + if err != nil { + return + } + err = addDashboard(collection_id) + if err != nil { + return + } + return +} + +func addCollection() (collection_id int, err error) { + payload := map[string]interface{}{ + "name": "temp_collection_meteor", + "color": "#ffffb3", + "description": "Temp Collection for Meteor Metabase Extractor", + } + + type response struct { + ID int `json:"id"` + } + var resp response + err = makeRequest("POST", host+"/api/collection", payload, &resp) + if err != nil { + return + } + + collection_id = resp.ID + return +} + +func addDashboard(collection_id int) (err error) { + payload := map[string]interface{}{ + "name": "random_dashboard", + "description": "some description", + "collection_id": collection_id, + } + + var dashboard metabase.Dashboard + err = makeRequest("POST", host+"/api/dashboard", payload, &dashboard) + if err != nil { + return + } + err = addCards(dashboard) + if err != nil { + return + } + + populatedDashboards = append(populatedDashboards, dashboard) + + return +} + +func addCards(dashboard metabase.Dashboard) (err error) { + // create card + cardPayload := map[string]interface{}{ + "name": "Orders, Filtered by Quantity", + "table_id": 1, + "database_id": 1, + "collection_id": dashboard.CollectionID, + "creator_id": 1, + "description": "HELPFUL CHART DESC", + "query_type": "query", + "display": "table", + "query_average_duration": 114, + "archived": false, + } + cardUrl := fmt.Sprintf("%s/api/card", host) + var card metabase.Card + err = makeRequest("POST", cardUrl, cardPayload, &card) + if err != nil { + return + } + + // set card to dashboard + cardToDashboardUrl := fmt.Sprintf("%s/api/dashboard/%d/cards", host, dashboard.ID) + err = makeRequest("POST", cardToDashboardUrl, map[string]interface{}{ + "card_id": card.ID, + }, nil) + if err != nil { + return + } + + // save card to memory for asserting + var cards []metabase.Card + cards = append(cards, card) + dashboardCards[dashboard.ID] = cards + + return +} + +func setSessionID(setup_token string) (err error) { + payload := map[string]interface{}{ + "user": map[string]interface{}{ + "first_name": "John", + "last_name": "Doe", + "email": email, + "password": pass, + "site_name": "Unaffiliated", + }, + "token": setup_token, + "prefs": map[string]interface{}{ + "site_name": "Unaffiliated", + "allow_tracking": "true", + }, + } + + type response struct { + ID string `json:"id"` + } + var resp response + err = makeRequest("POST", host+"/api/setup", payload, &resp) + if err != nil { + return + } + session_id = resp.ID + return +} + +func makeRequest(method, url string, payload interface{}, data interface{}) (err error) { + client := &http.Client{ + Timeout: 4 * time.Second, + } + + jsonBytes, err := json.Marshal(payload) + if err != nil { + return + } + req, err := http.NewRequest(method, url, bytes.NewBuffer(jsonBytes)) + if err != nil { + return + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-Metabase-Session", session_id) + + res, err := client.Do(req) + if err != nil { + return + } + b, err := ioutil.ReadAll(res.Body) + if err != nil { + return + } + err = json.Unmarshal(b, &data) + + return +} diff --git a/plugins/extractors/metabase/metabase.go b/plugins/extractors/metabase/metabase.go index a122a6605..ae50d9596 100644 --- a/plugins/extractors/metabase/metabase.go +++ b/plugins/extractors/metabase/metabase.go @@ -6,15 +6,17 @@ import ( _ "embed" // used to print the embedded assets "encoding/json" "fmt" - "github.com/pkg/errors" "io/ioutil" "net/http" - "strconv" "time" + "github.com/pkg/errors" + "google.golang.org/protobuf/types/known/timestamppb" + "github.com/odpf/meteor/models" "github.com/odpf/meteor/models/odpf/assets" "github.com/odpf/meteor/models/odpf/assets/common" + "github.com/odpf/meteor/models/odpf/assets/facets" "github.com/odpf/meteor/plugins" "github.com/odpf/meteor/registry" "github.com/odpf/meteor/utils" @@ -27,13 +29,15 @@ var summary string var sampleConfig = ` host: http://localhost:3000 user_id: meteor_tester -password: meteor_pass_1234` +password: meteor_pass_1234 +label: my-metabase` // Config hold the set of configuration for the metabase extractor type Config struct { - UserID string `mapstructure:"user_id" validate:"required"` - Password string `mapstructure:"password" validate:"required"` Host string `mapstructure:"host" validate:"required"` + Username string `mapstructure:"username" validate:"required"` + Password string `mapstructure:"password" validate:"required"` + Label string `mapstructure:"label" validate:"required"` SessionID string `mapstructure:"session_id"` } @@ -89,51 +93,117 @@ func (e *Extractor) Init(ctx context.Context, configMap map[string]interface{}) // Extract collects the metadata from the source. The metadata is collected through the out channel func (e *Extractor) Extract(ctx context.Context, emit plugins.Emit) (err error) { - dashboards, err := e.getDashboardsList() + dashboards, err := e.fetchDashboards() if err != nil { return errors.Wrap(err, "failed to fetch dashboard list") } - for _, dashboard := range dashboards { - data, err := e.buildDashboard(strconv.Itoa(dashboard.ID), dashboard.Name) + for _, d := range dashboards { + // we do not use "d" as the dashboard because it does not have + // "ordered_cards" field + dashboard, err := e.buildDashboard(d) if err != nil { return errors.Wrap(err, "failed to fetch dashboard data") } - emit(models.NewRecord(data)) + emit(models.NewRecord(dashboard)) } return nil } -func (e *Extractor) buildDashboard(id string, name string) (data *assets.Dashboard, err error) { - var dashboard Dashboard - if err = e.makeRequest("GET", e.config.Host+"/api/dashboard/"+id, nil, &dashboard); err != nil { - err = errors.Wrap(err, "failed to fetch dashboard") +func (e *Extractor) buildDashboard(d Dashboard) (data *assets.Dashboard, err error) { + // we fetch dashboard again individually to get more fields + dashboard, err := e.fetchDashboard(d.ID) + if err != nil { + err = errors.Wrapf(err, "error fetching dashboard") return } - var tempCards []*assets.Chart - for _, card := range dashboard.Charts { - var tempCard assets.Chart - tempCard.Source = "metabase" - tempCard.Urn = "metabase." + id + "." + strconv.Itoa(card.ID) - tempCard.DashboardUrn = "metabase." + name - tempCards = append(tempCards, &tempCard) + dashboardUrn := fmt.Sprintf("metabase::%s/dashboard/%d", e.config.Label, dashboard.ID) + + charts, err := e.buildCharts(dashboardUrn, dashboard) + if err != nil { + err = errors.Wrapf(err, "error building charts") + return } + + createdAt, updatedAt, err := e.buildTimestamps(dashboard.BaseModel) + if err != nil { + err = errors.Wrapf(err, "error building dashboard timestamps") + return + } + data = &assets.Dashboard{ Resource: &common.Resource{ - Urn: fmt.Sprintf("metabase.%s", dashboard.Name), + Urn: dashboardUrn, Name: dashboard.Name, Service: "metabase", }, Description: dashboard.Description, - Charts: tempCards, + Charts: charts, + Properties: &facets.Properties{ + Attributes: utils.TryParseMapToProto(map[string]interface{}{ + "id": dashboard.ID, + "collection_id": dashboard.CollectionID, + "creator_id": dashboard.CreatorID, + }), + }, + Timestamps: &common.Timestamp{ + CreateTime: timestamppb.New(createdAt), + UpdateTime: timestamppb.New(updatedAt), + }, } return } -func (e *Extractor) getDashboardsList() (data []Dashboard, err error) { - err = e.makeRequest("GET", e.config.Host+"/api/dashboard/", nil, &data) +func (e *Extractor) buildCharts(dashboardUrn string, dashboard Dashboard) (charts []*assets.Chart, err error) { + for _, oc := range dashboard.OrderedCards { + card := oc.Card + charts = append(charts, &assets.Chart{ + Urn: fmt.Sprintf("metabase::%s/card/%d", e.config.Label, card.ID), + DashboardUrn: dashboardUrn, + Source: "metabase", + Properties: &facets.Properties{ + Attributes: utils.TryParseMapToProto(map[string]interface{}{ + "id": card.ID, + "collection_id": card.CollectionID, + "creator_id": card.CreatorID, + "database_id": card.DatabaseID, + "table_id": card.TableID, + "query_average_duration": card.QueryAverageDuration, + "display": card.Display, + "archived": card.Archived, + }), + }, + }) + } + + return +} + +func (e *Extractor) buildTimestamps(model BaseModel) (createdAt time.Time, updatedAt time.Time, err error) { + createdAt, err = model.CreatedAt() if err != nil { + err = errors.Wrap(err, "failed parsing created_at") return } + updatedAt, err = model.UpdatedAt() + if err != nil { + err = errors.Wrap(err, "failed parsing updated_at") + return + } + + return +} + +func (e *Extractor) fetchDashboard(dashboard_id int) (dashboard Dashboard, err error) { + url := fmt.Sprintf("%s/api/dashboard/%d", e.config.Host, dashboard_id) + err = e.makeRequest("GET", url, nil, &dashboard) + + return +} + +func (e *Extractor) fetchDashboards() (data []Dashboard, err error) { + url := fmt.Sprintf("%s/api/dashboard", e.config.Host) + err = e.makeRequest("GET", url, nil, &data) + return } @@ -143,7 +213,7 @@ func (e *Extractor) getSessionID() (sessionID string, err error) { } payload := map[string]interface{}{ - "username": e.config.UserID, + "username": e.config.Username, "password": e.config.Password, } type responseID struct { @@ -159,32 +229,34 @@ func (e *Extractor) getSessionID() (sessionID string, err error) { // helper function to avoid rewriting a request func (e *Extractor) makeRequest(method, url string, payload interface{}, data interface{}) (err error) { - jsonifyPayload, err := json.Marshal(payload) + jsonBytes, err := json.Marshal(payload) if err != nil { return errors.Wrap(err, "failed to encode the payload JSON") } - body := bytes.NewBuffer(jsonifyPayload) - req, err := http.NewRequest(method, url, body) + + req, err := http.NewRequest(method, url, bytes.NewBuffer(jsonBytes)) if err != nil { return errors.Wrap(err, "failed to create request") } - if body != nil { - req.Header.Set("Content-Type", "application/json") - } - if e.config.SessionID != "" { - req.Header.Set("X-Metabase-Session", e.config.SessionID) - } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-Metabase-Session", e.config.SessionID) + res, err := e.client.Do(req) if err != nil { return errors.Wrap(err, "failed to generate response") } - b, err := ioutil.ReadAll(res.Body) + if res.StatusCode >= 300 { + return fmt.Errorf("getting %d status code", res.StatusCode) + } + + bytes, err := ioutil.ReadAll(res.Body) if err != nil { return errors.Wrap(err, "failed to read response body") } - if err = json.Unmarshal(b, &data); err != nil { - return errors.Wrapf(err, "failed to parse: %s", string(b)) + if err = json.Unmarshal(bytes, &data); err != nil { + return errors.Wrapf(err, "failed to parse: %s", string(bytes)) } + return } diff --git a/plugins/extractors/metabase/metabase_test.go b/plugins/extractors/metabase/metabase_test.go index 2b1addf3f..ffe04f15a 100644 --- a/plugins/extractors/metabase/metabase_test.go +++ b/plugins/extractors/metabase/metabase_test.go @@ -1,110 +1,37 @@ -//+build integration +//go:build integration +// +build integration package metabase_test import ( - "bytes" "context" "encoding/json" "fmt" - "github.com/odpf/meteor/test/utils" - "io/ioutil" - "log" - "net/http" - "os" - "strconv" "testing" - "time" + + "github.com/odpf/meteor/models" + testutils "github.com/odpf/meteor/test/utils" + "github.com/odpf/meteor/utils" + "google.golang.org/protobuf/types/known/timestamppb" "github.com/odpf/meteor/models/odpf/assets" + "github.com/odpf/meteor/models/odpf/assets/common" + "github.com/odpf/meteor/models/odpf/assets/facets" "github.com/odpf/meteor/plugins" "github.com/odpf/meteor/plugins/extractors/metabase" "github.com/odpf/meteor/test/mocks" - "github.com/ory/dockertest/v3" - "github.com/ory/dockertest/v3/docker" "github.com/stretchr/testify/assert" ) const ( - fname = "meteor" - lname = "metabase" - collectionName = "temp_collection_meteor" - collection_color = "#ffffb3" - collection_description = "Temp Collection for Meteor Metabase Extractor" - dashboard_name = "random_dashboard" - dashboard_description = "some description" - email = "meteorextractortestuser@gmail.com" - pass = "meteor_pass_1234" - port = "4002" -) - -var ( - client = &http.Client{ - Timeout: 4 * time.Second, - } - session_id = "" - collection_id = 1 - card_id = 0 - dashboard_id = 0 - host = "http://localhost:" + port + instanceLabel = "my-meta" ) -type responseID struct { - ID int `json:"id"` -} - -type sessionID struct { - ID string `json:"id"` -} - -func TestMain(m *testing.M) { - // setup test - opts := dockertest.RunOptions{ - Repository: "metabase/metabase", - Tag: "latest", - ExposedPorts: []string{port, "3000"}, - PortBindings: map[docker.Port][]docker.PortBinding{ - "3000": { - {HostIP: "0.0.0.0", HostPort: port}, - }, - }, - } - - retryFn := func(resource *dockertest.Resource) (err error) { - res, err := http.Get(host + "/api/health") - if err != nil { - return - } - if res.StatusCode != http.StatusOK { - return fmt.Errorf("received %d status code", res.StatusCode) - } - return - } - - // Exponential backoff-retry for container to be resy to accept connections - purgeFn, err := utils.CreateContainer(opts, retryFn) - if err != nil { - log.Fatal(err) - } - if err := setup(); err != nil { - log.Fatal(err) - } - - // Run tests - code := m.Run() - - // Clean tests - if err := purgeFn(); err != nil { - log.Fatal(err) - } - os.Exit(code) -} - func TestInit(t *testing.T) { t.Run("should return error for invalid config", func(t *testing.T) { - err := metabase.New(utils.Logger).Init(context.TODO(), map[string]interface{}{ - "user_id": "user", - "host": host, + err := metabase.New(testutils.Logger).Init(context.TODO(), map[string]interface{}{ + "username": "user", + "host": host, }) assert.Equal(t, plugins.InvalidConfigError{}, err) @@ -114,11 +41,12 @@ func TestInit(t *testing.T) { func TestExtract(t *testing.T) { t.Run("should return dashboard model", func(t *testing.T) { ctx := context.TODO() - extr := metabase.New(utils.Logger) + extr := metabase.New(testutils.Logger) err := extr.Init(ctx, map[string]interface{}{ - "user_id": email, - "password": pass, "host": host, + "username": email, + "password": pass, + "label": instanceLabel, "session_id": session_id, }) if err != nil { @@ -127,166 +55,84 @@ func TestExtract(t *testing.T) { emitter := mocks.NewEmitter() err = extr.Extract(ctx, emitter.Push) - assert.NoError(t, err) - var urns []string - for _, record := range emitter.Get() { - dashboard := record.Data().(*assets.Dashboard) - urns = append(urns, dashboard.Resource.Urn) + expected := expectedData() + records := emitter.Get() + var actuals []models.Metadata + for _, r := range records { + actuals = append(actuals, r.Data()) } - assert.Equal(t, []string{"metabase.random_dashboard"}, urns) - }) -} -func setup() (err error) { - type responseToken struct { - Token string `json:"setup-token"` - } - var data responseToken - err = makeRequest("GET", host+"/api/session/properties", nil, &data) - if err != nil { - return - } - setup_token := data.Token - err = setUser(setup_token) - if err != nil { - return - } - err = addMockData(session_id) - if err != nil { - return - } - return -} - -func setUser(setup_token string) (err error) { - payload := map[string]interface{}{ - "user": map[string]interface{}{ - "first_name": fname, - "last_name": lname, - "email": email, - "password": pass, - "site_name": "Unaffiliated", - }, - "token": setup_token, - "prefs": map[string]interface{}{ - "site_name": "Unaffiliated", - "allow_tracking": "true", - }, - } - var data sessionID - err = makeRequest("POST", host+"/api/setup", payload, &data) - if err != nil { - return - } - session_id = data.ID - err = getSessionID() - return + assert.Len(t, actuals, len(expected)) + assertJSON(t, expected, actuals) + }) } -func getSessionID() (err error) { - payload := map[string]interface{}{ - "username": email, - "password": pass, - } - var data sessionID - err = makeRequest("POST", host+"/api/session", payload, &data) - if err != nil { - return - } - session_id = data.ID - return -} +func expectedData() (records []*assets.Dashboard) { + for _, d := range populatedDashboards { + createdAt, _ := d.CreatedAt() + updatedAt, _ := d.UpdatedAt() + cards := dashboardCards[d.ID] + + dashboardUrn := fmt.Sprintf("metabase::%s/dashboard/%d", instanceLabel, d.ID) + var charts []*assets.Chart + for _, card := range cards { + charts = append(charts, &assets.Chart{ + Urn: fmt.Sprintf("metabase::%s/card/%d", instanceLabel, card.ID), + DashboardUrn: dashboardUrn, + Source: "metabase", + Name: card.Name, + Description: card.Description, + Properties: &facets.Properties{ + Attributes: utils.TryParseMapToProto(map[string]interface{}{ + "id": card.ID, + "collection_id": card.CollectionID, + "creator_id": card.CreatorID, + "database_id": card.DatabaseID, + "table_id": card.TableID, + "query_average_duration": card.QueryAverageDuration, + "display": card.Display, + "archived": card.Archived, + }), + }, + }) + } -func addMockData(session_id string) (err error) { - err = addCollection() - if err != nil { - return - } - err = addDashboard() - if err != nil { - return + records = append(records, &assets.Dashboard{ + Resource: &common.Resource{ + Urn: dashboardUrn, + Name: d.Name, + Service: "metabase", + }, + Description: d.Description, + Properties: &facets.Properties{ + Attributes: utils.TryParseMapToProto(map[string]interface{}{ + "id": d.ID, + "collection_id": d.CollectionID, + "creator_id": d.CreatorID, + }), + }, + Charts: charts, + Timestamps: &common.Timestamp{ + CreateTime: timestamppb.New(createdAt), + UpdateTime: timestamppb.New(updatedAt), + }, + }) } - return -} -func addCollection() (err error) { - payload := map[string]interface{}{ - "name": collectionName, - "color": collection_color, - "description": collection_description, - } - var data responseID - err = makeRequest("POST", host+"/api/collection", payload, &data) - if err != nil { - return - } - collection_id = data.ID return } -func addDashboard() (err error) { - payload := map[string]interface{}{ - "name": dashboard_name, - "description": dashboard_description, - "collection_id": collection_id, - } - - var data responseID - err = makeRequest("POST", host+"/api/dashboard", payload, &data) +func assertJSON(t *testing.T, expected interface{}, actual interface{}) { + actualBytes, err := json.Marshal(actual) if err != nil { - return + t.Fatal(err) } - dashboard_id = data.ID - err = addCard(dashboard_id) + expectedBytes, err := json.Marshal(expected) if err != nil { - return + t.Fatal(err) } - return -} -func addCard(id int) (err error) { - values := map[string]interface{}{ - "id": id, - } - x := strconv.Itoa(id) - type response struct { - ID int `json:"id"` - } - var data response - err = makeRequest("POST", host+"/api/dashboard/"+x+"/cards", values, &data) - if err != nil { - return - } - card_id = data.ID - return -} - -func makeRequest(method, url string, payload interface{}, data interface{}) (err error) { - jsonifyPayload, err := json.Marshal(payload) - if err != nil { - return - } - body := bytes.NewBuffer(jsonifyPayload) - req, err := http.NewRequest(method, url, body) - if err != nil { - return - } - if body != nil { - req.Header.Set("Content-Type", "application/json") - } - if session_id != "" { - req.Header.Set("X-Metabase-Session", session_id) - } - res, err := client.Do(req) - if err != nil { - return - } - b, err := ioutil.ReadAll(res.Body) - if err != nil { - return - } - err = json.Unmarshal(b, &data) - return + assert.Equal(t, string(expectedBytes), string(actualBytes)) } diff --git a/plugins/extractors/metabase/models.go b/plugins/extractors/metabase/models.go new file mode 100644 index 000000000..b754313fe --- /dev/null +++ b/plugins/extractors/metabase/models.go @@ -0,0 +1,98 @@ +package metabase + +import "time" + +const ( + timestampFormat = "2006-01-02T15:04:05.999999" +) + +type Dashboard struct { + BaseModel + CreatorID int `json:"creator_id"` + CollectionID int `json:"collection_id"` + Name string `json:"name"` + Description string `json:"description"` + OrderedCards []struct { + Card Card `json:"card"` + } `json:"ordered_cards"` + LastEditInfo struct { + Id string `json:"id"` + Email string `json:"email"` + Timestamp time.Time `json:"timestamp"` + } +} + +type Card struct { + BaseModel + CollectionID int `json:"collection_id"` + DatabaseID int `json:"database_id"` + TableID int `json:"table_id"` + CreatorID int `json:"creator_id"` + Name string `json:"name"` + QueryAverageDuration int `json:"query_average_duration"` + Description string `json:"description"` + Display string `json:"display"` + DatasetQuery struct { + Type string `json:"type"` + Query interface{} `json:"query"` + } `json:"dataset_query"` + Archived bool `json:"archived"` +} + +type CardResultMetadata struct { + BaseModel + Name string `json:"name"` + DisplayName string `json:"display_name"` + BaseType string `json:"base_type"` + EffectiveType string `json:"effective_type"` + SemanticType string `json:"semantic_type"` + Description string `json:"description"` + Unit string `json:"unit"` + FieldRef []string `json:"field_ref"` +} + +type Table struct { + BaseModel + DbID int `json:"db_id"` + Name string `json:"name"` + DisplayName string `json:"display_name"` + Description string `json:"description"` + FieldOrder string `json:"field_order"` + EntityType string `json:"entity_type"` + Schema string `json:"schema"` + Active bool `json:"active"` + Db Database `json:"db"` +} + +type Database struct { + BaseModel + DbID int `json:"db_id"` + Name string `json:"name"` + Features []string `json:"features"` + Description string `json:"description"` + Timezone string `json:"timezone"` + Engine string `json:"engine"` + MetadataSyncSchedule string `json:"metadata_sync_schedule"` + CacheFieldValuesSchedule string `json:"cache_field_values_schedule"` + AutoRunQueries bool `json:"auto_run_queries"` + IsFullSync bool `json:"is_full_sync"` + IsSample bool `json:"is_sample"` + IsOnDemand bool `json:"is_on_demand"` + Details struct { + Db string `json:"db"` + } `json:"details"` +} + +type BaseModel struct { + ID int `json:"id"` + CreatedAtString string `json:"created_at"` + UpdatedAtString string `json:"updated_at"` +} + +func (m *BaseModel) CreatedAt() (time.Time, error) { + return time.Parse(timestampFormat, m.CreatedAtString) +} + +func (m *BaseModel) UpdatedAt() (time.Time, error) { + return time.Parse(timestampFormat, m.UpdatedAtString) +} diff --git a/plugins/extractors/metabase/typeDefs.go b/plugins/extractors/metabase/typeDefs.go deleted file mode 100644 index 6e033debe..000000000 --- a/plugins/extractors/metabase/typeDefs.go +++ /dev/null @@ -1,18 +0,0 @@ -package metabase - -type Dashboard struct { - ID int `json:"id"` - Urn string - Name string `json:"name"` - Source string `default:"metabase"` - Description string `json:"description"` - Charts []Chart `json:"ordered_cards"` -} - -type Chart struct { - ID int `json:"id"` - Urn string - Source string `default:"metabase"` - DashboardUrn string - DashboardID int `json:"dashboard_id"` -}