From 6c582c18b16d335b1fc6de524b2d225ea7f80ddc Mon Sep 17 00:00:00 2001 From: Artem Date: Wed, 21 Jul 2021 18:27:46 +0300 Subject: [PATCH] Hasura REST API --- config/config.go | 1 + hasura/api.go | 23 +++++- hasura/hasura.go | 174 ++++++++++++++++++++++++++++---------------- hasura/requests.go | 103 +++++++++++++++++++++++++- hasura/responses.go | 2 +- 5 files changed, 236 insertions(+), 67 deletions(-) diff --git a/config/config.go b/config/config.go index fc04d94..a579625 100644 --- a/config/config.go +++ b/config/config.go @@ -57,6 +57,7 @@ type Hasura struct { Secret string `yaml:"admin_secret"` RowsLimit uint64 `yaml:"select_limit"` EnableAggregations bool `yaml:"allow_aggregation"` + Rest *bool `yaml:"rest"` } // Validate - diff --git a/hasura/api.go b/hasura/api.go index 36cdf5e..5f3b2e3 100644 --- a/hasura/api.go +++ b/hasura/api.go @@ -112,7 +112,7 @@ func (api *API) Health() error { } // ExportMetadata - -func (api *API) ExportMetadata(data interface{}) (ExportMetadataResponse, error) { +func (api *API) ExportMetadata(data *Metadata) (ExportMetadataResponse, error) { req := request{ Type: "export_metadata", Args: data, @@ -123,7 +123,7 @@ func (api *API) ExportMetadata(data interface{}) (ExportMetadataResponse, error) } // ReplaceMetadata - -func (api *API) ReplaceMetadata(data interface{}) error { +func (api *API) ReplaceMetadata(data *Metadata) error { req := request{ Type: "replace_metadata", Args: data, @@ -174,3 +174,22 @@ func (api *API) DropSelectPermissions(table, role string) error { } return api.post("/v1/query", nil, req, nil) } + +// CreateRestEndpoint - +func (api *API) CreateRestEndpoint(name, url, queryName, collectionName string) error { + req := request{ + Type: "create_rest_endpoint", + Args: map[string]interface{}{ + "name": name, + "url": url, + "methods": []string{"GET"}, + "definition": map[string]interface{}{ + "query": map[string]interface{}{ + "query_name": queryName, + "collection_name": collectionName, + }, + }, + }, + } + return api.post("/v1/query", nil, req, nil) +} diff --git a/hasura/hasura.go b/hasura/hasura.go index 1884c5c..37c7af4 100644 --- a/hasura/hasura.go +++ b/hasura/hasura.go @@ -1,6 +1,9 @@ package hasura import ( + "fmt" + "io/ioutil" + "os" "reflect" "strings" "time" @@ -12,6 +15,10 @@ import ( log "github.com/sirupsen/logrus" ) +const ( + allowedQueries = "allowed-queries" +) + // Create - creates hasura models func Create(hasura config.Hasura, cfg config.Database, views []string, models ...interface{}) error { api := New(hasura.URL, hasura.Secret) @@ -34,38 +41,34 @@ func Create(hasura config.Hasura, cfg config.Database, views []string, models .. log.Info("Merging metadata...") tables := make(map[string]struct{}) - dataTables := metadata["tables"].([]interface{}) - for i := range dataTables { - dataTable, ok := dataTables[i].(map[string]interface{}) - if !ok { - continue - } - table, ok := dataTable["table"].(map[string]interface{}) - if !ok { - continue - } - name := table["name"].(string) - tables[name] = struct{}{} + for i := range metadata.Tables { + tables[metadata.Tables[i].Schema.Name] = struct{}{} } for _, table := range export.Tables { - tableData, ok := table["table"].(map[string]interface{}) - if !ok { - continue - } - name := tableData["name"] - if _, ok := tables[name.(string)]; !ok { - dataTables = append(dataTables, table) + if _, ok := tables[table.Schema.Name]; !ok { + metadata.Tables = append(metadata.Tables, table) } } - metadata["tables"] = dataTables + if err := createQueryCollections(api, hasura, metadata); err != nil { + return err + } log.Info("Replacing metadata...") if err := api.ReplaceMetadata(metadata); err != nil { return err } + if hasura.Rest == nil || *hasura.Rest { + log.Info("Creating REST endpoints...") + for _, query := range metadata.QueryCollections[0].Definition.Queries { + if err := api.CreateRestEndpoint(query.Name, query.Name, query.Name, allowedQueries); err != nil { + return err + } + } + } + log.Info("Tracking views...") for i := range views { if err := api.TrackTable("public", views[i]); err != nil { @@ -79,7 +82,7 @@ func Create(hasura config.Hasura, cfg config.Database, views []string, models .. if err := api.CreateSelectPermissions(views[i], "user", Permission{ Limit: hasura.RowsLimit, AllowAggs: hasura.EnableAggregations, - Columns: "*", + Columns: Columns{"*"}, Filter: map[string]interface{}{}, }); err != nil { return err @@ -90,8 +93,8 @@ func Create(hasura config.Hasura, cfg config.Database, views []string, models .. } // Generate - creates hasura table structure in JSON from `models`. `models` should be pointer to your table models. `cfg` is DB config from YAML. -func Generate(hasura config.Hasura, cfg config.Database, models ...interface{}) (map[string]interface{}, error) { - tables := make([]interface{}, 0) +func Generate(hasura config.Hasura, cfg config.Database, models ...interface{}) (*Metadata, error) { + tables := make([]Table, 0) schema := getSchema(cfg) for _, model := range models { table, err := generateOne(hasura, schema, model) @@ -101,14 +104,14 @@ func Generate(hasura config.Hasura, cfg config.Database, models ...interface{}) tables = append(tables, table.HasuraSchema) } - return formatMetadata(tables), nil + return newMetadata(2, tables), nil } type table struct { Name string Schema string Columns []string - HasuraSchema map[string]interface{} + HasuraSchema Table } func newTable(schema, name string) table { @@ -118,6 +121,7 @@ func newTable(schema, name string) table { Name: name, } } + func generateOne(hasura config.Hasura, schema string, model interface{}) (table, error) { value := reflect.ValueOf(model) if value.Kind() != reflect.Ptr { @@ -129,53 +133,26 @@ func generateOne(hasura config.Hasura, schema string, model interface{}) (table, } t := newTable(schema, getTableName(value, typ)) - t.HasuraSchema = formatTable(t.Name, t.Schema) + t.HasuraSchema = newMetadataTable(t.Name, t.Schema) t.Columns = getColumns(typ) - if p, ok := t.HasuraSchema["select_permissions"]; ok { - t.HasuraSchema["select_permissions"] = append(p.([]interface{}), formatSelectPermissions(hasura.RowsLimit, hasura.EnableAggregations, t.Columns...)) - } else { - t.HasuraSchema["select_permissions"] = []interface{}{ - formatSelectPermissions(hasura.RowsLimit, hasura.EnableAggregations, t.Columns...), - } - } - t.HasuraSchema["object_relationships"] = []interface{}{} - t.HasuraSchema["array_relationships"] = []interface{}{} + t.HasuraSchema.SelectPermissions = append(t.HasuraSchema.SelectPermissions, formatSelectPermissions(hasura.RowsLimit, hasura.EnableAggregations, t.Columns...)) return t, nil } -func formatSelectPermissions(limit uint64, allowAggs bool, columns ...string) map[string]interface{} { +func formatSelectPermissions(limit uint64, allowAggs bool, columns ...string) SelectPermission { if limit == 0 { limit = 10 } - return map[string]interface{}{ - "role": "user", - "permission": map[string]interface{}{ - "columns": columns, - "filter": map[string]interface{}{}, - "allow_aggregations": allowAggs, - "limit": limit, - }, - } -} - -func formatTable(name, schema string) map[string]interface{} { - return map[string]interface{}{ - "table": map[string]interface{}{ - "schema": schema, - "name": name, + return SelectPermission{ + Role: "user", + Permission: Permission{ + Columns: columns, + Filter: map[string]interface{}{}, + AllowAggs: allowAggs, + Limit: limit, }, - "object_relationships": []interface{}{}, - "array_relationships": []interface{}{}, - "select_permissions": []interface{}{}, - } -} - -func formatMetadata(tables []interface{}) map[string]interface{} { - return map[string]interface{}{ - "version": 2, - "tables": tables, } } @@ -193,7 +170,6 @@ func getTableName(value reflect.Value, typ reflect.Type) string { return res[0].String() } -// TODO: parsing schema from connection string func getSchema(cfg config.Database) string { return "public" } @@ -214,3 +190,75 @@ func getColumns(typ reflect.Type) []string { } return columns } + +func createQueryCollections(api *API, cfg config.Hasura, metadata *Metadata) error { + if metadata == nil { + return nil + } + + files, err := ioutil.ReadDir("graphql") + if err != nil { + if os.IsNotExist(err) { + return nil + } + return err + } + + queries := make([]Query, 0) + for i := range files { + name := files[i].Name() + if !strings.HasSuffix(name, ".graphql") { + continue + } + + queryName := strings.TrimSuffix(name, ".graphql") + + f, err := os.Open(fmt.Sprintf("graphql/%s", name)) + if err != nil { + return err + } + defer f.Close() + + data, err := ioutil.ReadAll(f) + if err != nil { + return err + } + + queries = append(queries, Query{ + Name: queryName, + Query: string(data), + }) + } + + if len(metadata.QueryCollections) > 0 && len(metadata.QueryCollections[0].Definition.Queries) > 0 { + metadata.QueryCollections[0].Definition.Queries = mergeQueries(queries, metadata.QueryCollections[0].Definition.Queries) + } else { + metadata.QueryCollections = []QueryCollection{ + { + Name: allowedQueries, + Definition: Definition{ + Queries: queries, + }, + }, + } + } + + return nil +} + +func mergeQueries(a []Query, b []Query) []Query { + for i := range a { + var found bool + for j := range b { + if b[j].Name == a[i].Name { + found = true + break + } + } + + if !found { + b = append(b, a[i]) + } + } + return b +} diff --git a/hasura/requests.go b/hasura/requests.go index 45ebf18..521df5e 100644 --- a/hasura/requests.go +++ b/hasura/requests.go @@ -1,5 +1,7 @@ package hasura +import "github.com/pkg/errors" + type request struct { Type string `json:"type"` Args interface{} `json:"args"` @@ -7,8 +9,107 @@ type request struct { // Permission - type Permission struct { - Columns string `json:"columns"` + Columns Columns `json:"columns"` Limit uint64 `json:"limit"` AllowAggs bool `json:"allow_aggregations"` Filter interface{} `json:"filter,omitempty"` } + +// Metadata - +type Metadata struct { + Version int `json:"version"` + Tables []Table `json:"tables"` + QueryCollections []QueryCollection `json:"query_collections,omitempty"` +} + +func newMetadata(version int, tables []Table) *Metadata { + return &Metadata{ + Version: version, + Tables: tables, + } +} + +// Table - +type Table struct { + ObjectRelationships []interface{} `json:"object_relationships"` + ArrayRelationships []interface{} `json:"array_relationships"` + SelectPermissions []SelectPermission `json:"select_permissions"` + Schema TableSchema `json:"table"` +} + +func newMetadataTable(name, schema string) Table { + return Table{ + ObjectRelationships: make([]interface{}, 0), + ArrayRelationships: make([]interface{}, 0), + SelectPermissions: make([]SelectPermission, 0), + Schema: TableSchema{ + Name: name, + Schema: schema, + }, + } +} + +// TableSchema - +type TableSchema struct { + Schema string `json:"schema"` + Name string `json:"name"` +} + +// SelectPermission - +type SelectPermission struct { + Role string `json:"role"` + Permission Permission `json:"permission"` +} + +// Columns - +type Columns []string + +// UnmarshalJSON - +func (columns *Columns) UnmarshalJSON(data []byte) error { + var val interface{} + if err := json.Unmarshal(data, &val); err != nil { + return err + } + + *columns = make(Columns, 0) + switch typ := val.(type) { + case string: + *columns = append(*columns, typ) + case []interface{}: + for i := range typ { + if s, ok := typ[i].(string); ok { + *columns = append(*columns, s) + } + } + default: + return errors.Errorf("Invalid columns type: %T", typ) + } + return nil +} + +// MarshalJSON - +func (columns Columns) MarshalJSON() ([]byte, error) { + if len(columns) == 1 && columns[0] == "*" { + return []byte(`"*"`), nil + } + s := []string(columns) + return json.Marshal(s) +} + +// QueryCollection - +type QueryCollection struct { + Definition Definition `json:"definition"` + Name string `json:"name"` +} + +// Definition - +type Definition struct { + Queries []Query `json:"queries"` +} + +// Query - +type Query struct { + Name string `json:"name"` + Query string `json:"query,omitempty"` + CollectionName string `json:"collection_name,omitempty"` +} diff --git a/hasura/responses.go b/hasura/responses.go index d73314b..018a30a 100644 --- a/hasura/responses.go +++ b/hasura/responses.go @@ -6,5 +6,5 @@ type replaceMetadataResponse struct { // ExportMetadataResponse - type ExportMetadataResponse struct { - Tables []map[string]interface{} `json:"tables"` + Tables []Table `json:"tables"` }