diff --git a/.golangci.yml b/.golangci.yml index ad7f9dff..779c4e6e 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -2,6 +2,8 @@ run: allow-parallel-runners: true timeout: 5m linters-settings: + unused: + local-variables-are-used: true gosec: excludes: - G402 diff --git a/cmd/baserow/main.go b/cmd/baserow/main.go index ae229875..1fa5dade 100644 --- a/cmd/baserow/main.go +++ b/cmd/baserow/main.go @@ -52,10 +52,10 @@ func main() { Action: baserow.CreateAccessToken, }, { - Name: "create-table", - Usage: "Create a baserow database table", - Flags: baserow.CreateTableFlag(), - Action: baserow.CreateTable, + Name: "create-ontology-table", + Usage: "Create a baserow table with ontology fields preset", + Flags: baserow.CreateOntologyTableFlag(), + Action: baserow.CreateOntologyTableHandler, }, { Name: "load-ontology", diff --git a/internal/baserow/cli/action.go b/internal/baserow/cli/action.go new file mode 100644 index 00000000..d500e4c1 --- /dev/null +++ b/internal/baserow/cli/action.go @@ -0,0 +1,160 @@ +package cli + +import ( + "context" + "fmt" + + "slices" + + "github.com/dictyBase/modware-import/internal/baserow/client" + "github.com/dictyBase/modware-import/internal/baserow/database" + "github.com/dictyBase/modware-import/internal/baserow/ontology" + "github.com/dictyBase/modware-import/internal/collection" + "github.com/dictyBase/modware-import/internal/registry" + "github.com/urfave/cli/v2" +) + +func CreateDatabaseToken(cltx *cli.Context) error { + atoken, err := database.AccessToken(&database.AccessTokenProperties{ + Email: cltx.String("email"), + Password: cltx.String("password"), + Server: cltx.String("server"), + }) + if err != nil { + return cli.Exit(fmt.Errorf("error in creating access token %s", err), 2) + } + bclient := database.BaserowClient(cltx.String("server")) + authCtx := context.WithValue( + context.Background(), + client.ContextAccessToken, + atoken, + ) + wlist, r, err := bclient.WorkspacesApi.ListWorkspaces(authCtx). + Execute() + defer r.Body.Close() + if err != nil { + return cli.Exit( + fmt.Errorf("error in executing list workspaces API call %s", err), + 2, + ) + } + wnames := collection.Map( + wlist, + func(w client.WorkspaceUserWorkspace) string { return w.GetName() }, + ) + idx := slices.Index(wnames, cltx.String("workspace")) + if idx == -1 { + return cli.Exit( + fmt.Errorf( + "workspace %s cannot be found", + cltx.String("workspace"), + ), + 2, + ) + } + tok, r, err := bclient.DatabaseTokensApi.CreateDatabaseToken(authCtx). + TokenCreate(client.TokenCreate{ + Name: cltx.String("name"), + Workspace: wlist[idx].GetId(), + }). + Execute() + defer r.Body.Close() + if err != nil { + return cli.Exit( + fmt.Errorf("error in creating token %s", err), + 2, + ) + } + fmt.Printf("database token %s\n", tok.GetKey()) + return nil +} + +func CreateAccessToken(cltx *cli.Context) error { + token, err := database.AccessToken(&database.AccessTokenProperties{ + Email: cltx.String("email"), + Password: cltx.String("password"), + Server: cltx.String("server"), + }) + if err != nil { + return cli.Exit(err, 2) + } + fmt.Println(token) + return nil +} + +func LoadOntologyToTable(cltx *cli.Context) error { + logger := registry.GetLogger() + bclient := database.BaserowClient(cltx.String("server")) + authCtx := context.WithValue( + context.Background(), + client.ContextDatabaseToken, + cltx.String("token"), + ) + ontTbl := &database.OntologyTableManager{ + TableManager: &database.TableManager{ + Client: bclient, + Logger: logger, + Ctx: authCtx, + Token: cltx.String("token"), + DatabaseId: int32(cltx.Int("database-id")), + }, + } + ok, err := ontTbl.CheckAllTableFields( + &client.Table{Id: int32(cltx.Int("table-id"))}, + ) + if err != nil { + return cli.Exit(err.Error(), 2) + } + if !ok { + return cli.Exit("table does not have the required fields", 2) + } + props := &ontology.LoadProperties{ + File: cltx.String("input"), + TableId: cltx.Int("table-id"), + Token: cltx.String("token"), + Client: bclient, + Logger: logger, + } + if err := ontology.LoadNewOrUpdate(props); err != nil { + return cli.Exit(err.Error(), 2) + } + + return nil +} + +func CreateOntologyTableHandler(cltx *cli.Context) error { + logger := registry.GetLogger() + bclient := database.BaserowClient(cltx.String("server")) + authCtx := context.WithValue( + context.Background(), + client.ContextAccessToken, + cltx.String("token"), + ) + ontTbl := &database.OntologyTableManager{ + TableManager: &database.TableManager{ + Client: bclient, + Logger: logger, + Ctx: authCtx, + Token: cltx.String("token"), + DatabaseId: int32(cltx.Int("database-id")), + }, + } + tbl, err := ontTbl.CreateTable(cltx.String("table"), ontTbl.FieldNames()) + if err != nil { + return cli.Exit(fmt.Sprintf("error in creating table %s", err), 2) + } + logger.Infof("created table with fields %s", tbl.GetName()) + msg, err := ontTbl.UpdateField( + tbl, + "is_obsolete", + map[string]interface{}{"name": "is_obsolete", "type": "boolean"}, + ) + if err != nil { + return cli.Exit( + fmt.Sprintf("error in updating is_obsolete field %s", err), + 2, + ) + } + logger.Info(msg) + return nil +} diff --git a/internal/baserow/cli/database.go b/internal/baserow/cli/database.go deleted file mode 100644 index da9a7985..00000000 --- a/internal/baserow/cli/database.go +++ /dev/null @@ -1,147 +0,0 @@ -package cli - -import ( - "context" - "errors" - "fmt" - - "net/http" - - "github.com/dictyBase/modware-import/internal/baserow/client" - "github.com/dictyBase/modware-import/internal/collection" - "github.com/urfave/cli/v2" - "golang.org/x/exp/slices" -) - -type accessTokenProperties struct { - email, password, server string -} - -func baserowClient(server string) *client.APIClient { - conf := client.NewConfiguration() - conf.Host = server - conf.Scheme = "https" - return client.NewAPIClient(conf) -} - -func accessToken(args *accessTokenProperties) (string, error) { - req := baserowClient( - args.server, - ).UserApi.TokenAuth( - context.Background(), - ) - resp, r, err := req.TokenObtainPairWithUser( - client.TokenObtainPairWithUser{ - Email: &args.email, - Password: args.password, - }, - ).Execute() - defer r.Body.Close() - if err != nil { - return "", fmt.Errorf("error in executing API call %s", err) - } - if r != nil && r.StatusCode == http.StatusUnauthorized { - return "", errors.New("unauthrorized access") - } - return resp.GetToken(), nil -} - -func CreateAccessToken(c *cli.Context) error { - token, err := accessToken(&accessTokenProperties{ - email: c.String("email"), - password: c.String("password"), - server: c.String("server"), - }) - if err != nil { - return cli.Exit(err, 2) - } - fmt.Println(token) - return nil -} - -func CreateDatabaseToken(c *cli.Context) error { - atoken, err := accessToken(&accessTokenProperties{ - email: c.String("email"), - password: c.String("password"), - server: c.String("server"), - }) - if err != nil { - return cli.Exit(fmt.Errorf("error in creating access token %s", err), 2) - } - bclient := baserowClient(c.String("server")) - authCtx := context.WithValue( - context.Background(), - client.ContextAccessToken, - atoken, - ) - wlist, r, err := bclient.WorkspacesApi.ListWorkspaces(authCtx). - Execute() - defer r.Body.Close() - if err != nil { - return cli.Exit( - fmt.Errorf("error in executing list workspaces API call %s", err), - 2, - ) - } - wnames := collection.Map( - wlist, - func(w client.WorkspaceUserWorkspace) string { return w.GetName() }, - ) - idx := slices.Index(wnames, c.String("workspace")) - if idx == -1 { - return cli.Exit( - fmt.Errorf("workspace %s cannot be found", c.String("workspace")), - 2, - ) - } - tok, r, err := bclient.DatabaseTokensApi.CreateDatabaseToken(authCtx). - TokenCreate(client.TokenCreate{ - Name: c.String("name"), - Workspace: wlist[idx].GetId(), - }). - Execute() - defer r.Body.Close() - if err != nil { - return cli.Exit( - fmt.Errorf("error in creating token %s", err), - 2, - ) - } - fmt.Printf("database token %s\n", tok.GetKey()) - return nil -} - -func CreateDatabaseTokenFlag() []cli.Flag { - aflags := CreateAccessTokenFlag() - return append(aflags, []cli.Flag{ - &cli.StringFlag{ - Name: "workspace", - Aliases: []string{"w"}, - Usage: "Only tables under this workspaces can be accessed", - Required: true, - }, - &cli.StringFlag{ - Name: "name", - Aliases: []string{"n"}, - Usage: "token name", - Required: true, - }, - }...) -} - -func CreateAccessTokenFlag() []cli.Flag { - return []cli.Flag{ - &cli.StringFlag{ - Name: "email", - Aliases: []string{"e"}, - Usage: "Email of the user", - Required: true, - }, - &cli.StringFlag{ - Name: "password", - Aliases: []string{"p"}, - Usage: "Database password", - Required: true, - }, - } -} diff --git a/internal/baserow/cli/flag.go b/internal/baserow/cli/flag.go new file mode 100644 index 00000000..2b9a593f --- /dev/null +++ b/internal/baserow/cli/flag.go @@ -0,0 +1,81 @@ +package cli + +import "github.com/urfave/cli/v2" + +func CreateAccessTokenFlag() []cli.Flag { + return []cli.Flag{ + &cli.StringFlag{ + Name: "email", + Aliases: []string{"e"}, + Usage: "Email of the user", + Required: true, + }, + &cli.StringFlag{ + Name: "password", + Aliases: []string{"p"}, + Usage: "Database password", + Required: true, + }, + } +} + +func CreateDatabaseTokenFlag() []cli.Flag { + aflags := CreateAccessTokenFlag() + return append(aflags, []cli.Flag{ + &cli.StringFlag{ + Name: "workspace", + Aliases: []string{"w"}, + Usage: "Only tables under this workspaces can be accessed", + Required: true, + }, + &cli.StringFlag{ + Name: "name", + Aliases: []string{"n"}, + Usage: "token name", + Required: true, + }, + }...) +} + +func LoadOntologyToTableFlag() []cli.Flag { + return []cli.Flag{ + &cli.StringFlag{ + Name: "token", + Aliases: []string{"t"}, + Usage: "database token with write privilege", + Required: true, + }, + &cli.IntFlag{ + Name: "table-id", + Usage: "Database table id", + Required: true, + }, + &cli.StringFlag{ + Name: "input", + Aliases: []string{"i"}, + Usage: "input json formatted ontology file", + Required: true, + }, + } +} + +func CreateOntologyTableFlag() []cli.Flag { + return []cli.Flag{ + &cli.StringFlag{ + Name: "token", + Aliases: []string{"t"}, + Usage: "database token with write privilege", + Required: true, + }, + &cli.IntFlag{ + Name: "database-id", + Usage: "Database id", + Required: true, + }, + &cli.StringFlag{ + Name: "table", + Usage: "Ontology table name", + Required: true, + }, + } +} diff --git a/internal/baserow/cli/table.go b/internal/baserow/cli/table.go deleted file mode 100644 index 4f3d5dc9..00000000 --- a/internal/baserow/cli/table.go +++ /dev/null @@ -1,135 +0,0 @@ -package cli - -import ( - "context" - "fmt" - - "github.com/dictyBase/modware-import/internal/baserow/client" - "github.com/dictyBase/modware-import/internal/registry" - "github.com/urfave/cli/v2" -) - -func LoadOntologyToTable(c *cli.Context) error { - logger := registry.GetLogger() - bclient := baserowClient(c.String("server")) - authCtx := context.WithValue( - context.Background(), - client.ContextDatabaseToken, - c.String("token"), - ) - tlist, resp, err := bclient. - DatabaseTableFieldsApi. - ListDatabaseTableFields(authCtx, int32(c.Int("table-id"))). - Execute() - if err != nil { - return cli.Exit( - fmt.Sprintf("error in getting list of table fields %s", err), 2, - ) - } - defer resp.Body.Close() - if len(tlist) == 0 { - logger.Debug("need to create fields in the table") - _, trsp, err := bclient. - DatabaseTableFieldsApi. - CreateDatabaseTableField(authCtx, int32(c.Int("table-id"))). - FieldCreateField(client.FieldCreateField{ - BooleanFieldCreateField: client.NewBooleanFieldCreateField( - "Is_obsolete", - client.BOOLEAN, - ), - }).Execute() - if err != nil { - return cli.Exit( - fmt.Errorf( - "error in creating table field Is_obsolete %s", - err, - ), - 2, - ) - } - logger.Info("created field Is_obsolete") - - defer trsp.Body.Close() - for _, field := range []string{"Name", "Id"} { - _, frsp, err := bclient. - DatabaseTableFieldsApi. - CreateDatabaseTableField(authCtx, int32(c.Int("table-id"))). - FieldCreateField(client.FieldCreateField{ - TextFieldCreateField: client.NewTextFieldCreateField( - field, - client.TEXT, - ), - }).Execute() - if err != nil { - return cli.Exit( - fmt.Errorf( - "error in creating table field %s %s", - field, err, - ), - 2, - ) - } - defer frsp.Body.Close() - logger.Infof("created field %s", field) - } - } - return nil -} - -func CreateTable(c *cli.Context) error { - logger := registry.GetLogger() - bclient := baserowClient(c.String("server")) - authCtx := context.WithValue( - context.Background(), - client.ContextAccessToken, - c.String("token"), - ) - tbl, resp, err := bclient.DatabaseTablesApi.CreateDatabaseTable(authCtx, int32(c.Int("database-id"))). - TableCreate(client.TableCreate{Name: c.String("table")}). - Execute() - if err != nil { - return cli.Exit( - fmt.Errorf("error in creating table %s", err), 2, - ) - } - defer resp.Body.Close() - logger.Infof("created table %s", tbl.GetName()) - return nil -} - -func CreateTableFlag() []cli.Flag { - return []cli.Flag{ - &cli.StringFlag{ - Name: "token", - Aliases: []string{"t"}, - Usage: "database token with write privilege", - Required: true, - }, - &cli.IntFlag{ - Name: "database-id", - Usage: "Database id", - Required: true, - }, - &cli.StringFlag{ - Name: "table", - Usage: "Database table", - Required: true, - }, - } -} - -func LoadOntologyToTableFlag() []cli.Flag { - return []cli.Flag{ - &cli.StringFlag{ - Name: "token", - Aliases: []string{"t"}, - Usage: "database token with write privilege", - Required: true, - }, - &cli.IntFlag{ - Name: "table-id", - Usage: "Database table id", - Required: true, - }, - } -} diff --git a/internal/baserow/database/database.go b/internal/baserow/database/database.go new file mode 100644 index 00000000..559809af --- /dev/null +++ b/internal/baserow/database/database.go @@ -0,0 +1,44 @@ +package database + +import ( + "context" + "errors" + "fmt" + + "net/http" + + "github.com/dictyBase/modware-import/internal/baserow/client" +) + +type AccessTokenProperties struct { + Email, Password, Server string +} + +func BaserowClient(server string) *client.APIClient { + conf := client.NewConfiguration() + conf.Host = server + conf.Scheme = "https" + return client.NewAPIClient(conf) +} + +func AccessToken(args *AccessTokenProperties) (string, error) { + req := BaserowClient( + args.Server, + ).UserApi.TokenAuth( + context.Background(), + ) + resp, r, err := req.TokenObtainPairWithUser( + client.TokenObtainPairWithUser{ + Email: &args.Email, + Password: args.Password, + }, + ).Execute() + defer r.Body.Close() + if err != nil { + return "", fmt.Errorf("error in executing API call %s", err) + } + if r != nil && r.StatusCode == http.StatusUnauthorized { + return "", errors.New("unauthrorized access") + } + return resp.GetToken(), nil +} diff --git a/internal/baserow/database/functional_handlers.go b/internal/baserow/database/functional_handlers.go new file mode 100644 index 00000000..984b3161 --- /dev/null +++ b/internal/baserow/database/functional_handlers.go @@ -0,0 +1,128 @@ +package database + +import ( + "net/http" + + H "github.com/IBM/fp-go/context/readerioeither/http" + F "github.com/IBM/fp-go/function" + "github.com/dictyBase/modware-import/internal/baserow/client" +) + +var ( + makeHTTPRequest = F.Bind13of3(H.MakeRequest) + readFieldDelResp = H.ReadJSON[tableFieldDelResponse]( + H.MakeClient(http.DefaultClient), + ) + readFieldsResp = H.ReadJSON[[]tableFieldRes]( + H.MakeClient(http.DefaultClient), + ) + readUpdateFieldsResp = H.ReadJSON[tableFieldUpdateResponse]( + H.MakeClient(http.DefaultClient), + ) + readTableCreateResp = H.ReadJSON[tableFieldRes]( + H.MakeClient(http.DefaultClient), + ) + HasField = F.Curry2(uncurriedHasField) + ResToReqTableWithParams = F.Curry2(uncurriedResToReqTableWithParams) +) + +type tableFieldUpdateResponse struct { + Id int `json:"id"` + TableId int `json:"table_id"` +} + +type jsonPayload struct { + Error error + Payload []byte +} + +type tableFieldDelResponse struct { + RelatedFields []struct { + ID int `json:"id"` + TableID int `json:"table_id"` + } `json:"related_fields,omitempty"` +} + +type fieldsReqFeedback struct { + Error error + Fields []tableFieldRes + Msg string + Table *client.Table +} + +type tableFieldRes struct { + Name string `json:"name"` + Id int `json:"id"` +} + +type tableFieldReq struct { + tableFieldRes + Params map[string]interface{} +} + +func uncurriedHasField(name string, fieldResp tableFieldRes) bool { + return fieldResp.Name == name +} + +func onTableCreateFeedbackSuccess(res tableFieldRes) fieldsReqFeedback { + return fieldsReqFeedback{ + Table: &client.Table{ + Id: int32(res.Id), + Name: res.Name, + }, + } +} + +func onFieldsReqFeedbackError(err error) fieldsReqFeedback { + return fieldsReqFeedback{Error: err} +} + +func onFieldsReqFeedbackSuccess(resp []tableFieldRes) fieldsReqFeedback { + return fieldsReqFeedback{Fields: resp} +} + +func onFieldDelReqFeedbackSuccess( + resp tableFieldDelResponse, +) fieldsReqFeedback { + return fieldsReqFeedback{Msg: "deleted field"} +} + +func onFieldUpdateReqFeedbackSuccess( + resp tableFieldUpdateResponse, +) fieldsReqFeedback { + return fieldsReqFeedback{Msg: "updated field"} +} + +func onFieldDelReqFeedbackNone() fieldsReqFeedback { + return fieldsReqFeedback{Msg: "no field found to delete"} +} + +func onJSONPayloadError(err error) jsonPayload { + return jsonPayload{Error: err} +} + +func onJSONPayloadSuccess(resp []byte) jsonPayload { + return jsonPayload{Payload: resp} +} + +func uncurriedResToReqTableWithParams( + params map[string]interface{}, + req tableFieldRes, +) tableFieldReq { + return tableFieldReq{ + tableFieldRes: tableFieldRes{ + Name: req.Name, + Id: req.Id, + }, + Params: params, + } +} + +func ResToReqTable(req tableFieldRes) tableFieldReq { + return tableFieldReq{ + tableFieldRes: tableFieldRes{ + Name: req.Name, + Id: req.Id, + }, + } +} diff --git a/internal/baserow/database/ontology.go b/internal/baserow/database/ontology.go new file mode 100644 index 00000000..b1640880 --- /dev/null +++ b/internal/baserow/database/ontology.go @@ -0,0 +1,17 @@ +package database + +type OntologyTableManager struct { + *TableManager +} + +func (ont *OntologyTableManager) FieldNames() []string { + return []string{"term_id", "name", "is_obsolete"} +} + +func (ont *OntologyTableManager) FieldDefs() []map[string]interface{} { + return []map[string]interface{}{ + {"name": "name", "type": "text"}, + {"name": "term_id", "type": "text"}, + {"name": "is_obsolete", "type": "boolean"}, + } +} diff --git a/internal/baserow/database/table.go b/internal/baserow/database/table.go new file mode 100644 index 00000000..7c3ddd7a --- /dev/null +++ b/internal/baserow/database/table.go @@ -0,0 +1,276 @@ +package database + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + + A "github.com/IBM/fp-go/array" + J "github.com/IBM/fp-go/json" + O "github.com/IBM/fp-go/option" + + "github.com/dictyBase/modware-import/internal/collection" + "golang.org/x/exp/slices" + + "github.com/dictyBase/modware-import/internal/baserow/client" + "github.com/dictyBase/modware-import/internal/baserow/httpapi" + "github.com/sirupsen/logrus" + + R "github.com/IBM/fp-go/context/readerioeither" + H "github.com/IBM/fp-go/context/readerioeither/http" + E "github.com/IBM/fp-go/either" + F "github.com/IBM/fp-go/function" +) + +type FieldDefinition interface { + FieldNames() []string + FieldDefs() map[string]interface{} +} + +type TableManager struct { + FieldDefinition + Logger *logrus.Entry + Client *client.APIClient + Ctx context.Context + Token string + DatabaseId int32 +} + +func (tbm *TableManager) TableFieldsChangeURL( + req tableFieldReq, +) string { + return fmt.Sprintf( + "https://%s/api/database/fields/%d/", + tbm.Client.GetConfig().Host, + req.Id, + ) +} + +func (tbm *TableManager) TableFieldsURL(tbl *client.Table) string { + return fmt.Sprintf( + "https://%s/api/database/fields/table/%d/", + tbm.Client.GetConfig().Host, + tbl.GetId(), + ) +} + +func (tbm *TableManager) CreteTableURL() string { + return fmt.Sprintf( + "https://%s/api/database/tables/database/%d/", + tbm.Client.GetConfig().Host, + tbm.DatabaseId, + ) +} + +func (tbm *TableManager) CreateTable( + table string, fields []string, +) (*client.Table, error) { + var row []interface{} + params := map[string]interface{}{ + "name": table, + "data": append(row, fields), + "first_row_header": "true", + } + createPayload := F.Pipe2( + params, + J.Marshal, + E.Fold(onJSONPayloadError, onJSONPayloadSuccess), + ) + if createPayload.Error != nil { + return &client.Table{}, createPayload.Error + } + resp := F.Pipe3( + tbm.CreteTableURL(), + makeHTTPRequest("POST", bytes.NewBuffer(createPayload.Payload)), + R.Map(httpapi.SetHeaderWithJWT(tbm.Token)), + readTableCreateResp, + )(context.Background()) + output := F.Pipe1( + resp(), + E.Fold[error, tableFieldRes, fieldsReqFeedback]( + onFieldsReqFeedbackError, + onTableCreateFeedbackSuccess, + ), + ) + + return output.Table, output.Error +} + +func (tbm *TableManager) TableFieldsResp( + tbl *client.Table, +) (*http.Response, error) { + reqURL := fmt.Sprintf( + "https://%s/api/database/fields/table/%d/", + tbm.Client.GetConfig().Host, + tbl.GetId(), + ) + req, err := http.NewRequest("GET", reqURL, nil) + if err != nil { + return nil, fmt.Errorf("error in creating request %s ", err) + } + httpapi.CommonHeader(req, tbm.Token, "Token") + return httpapi.ReqToResponse(req) +} + +func (tbm *TableManager) ListTableFields( + tbl *client.Table, +) ([]tableFieldRes, error) { + resp := F.Pipe3( + tbm.TableFieldsURL(tbl), + H.MakeGetRequest, + R.Map(httpapi.SetHeaderWithJWT(tbm.Token)), + readFieldsResp, + )(context.Background()) + output := F.Pipe1( + resp(), + E.Fold[error, []tableFieldRes, fieldsReqFeedback]( + onFieldsReqFeedbackError, + onFieldsReqFeedbackSuccess, + ), + ) + return output.Fields, output.Error +} + +func (tbm *OntologyTableManager) CreateFields(tbl *client.Table) error { + reqURL := fmt.Sprintf( + "https://%s/api/database/fields/table/%d/", + tbm.Client.GetConfig().Host, + tbl.GetId(), + ) + for _, params := range tbm.FieldDefs() { + jsonData, err := json.Marshal(params) + if err != nil { + return fmt.Errorf("error in encoding body %s", err) + } + req, err := http.NewRequest("POST", reqURL, bytes.NewBuffer(jsonData)) + if err != nil { + return fmt.Errorf("error in creating request %s ", err) + } + httpapi.CommonHeader(req, tbm.Token, "JWT") + res, err := httpapi.ReqToResponse(req) + if err != nil { + return err + } + defer res.Body.Close() + } + + return nil +} + +func (tbm *OntologyTableManager) CheckAllTableFields( + tbl *client.Table, +) (bool, error) { + ok := false + res, err := tbm.TableFieldsResp(tbl) + if err != nil { + return ok, err + } + defer res.Body.Close() + existing := make([]tableFieldRes, 0) + if err := json.NewDecoder(res.Body).Decode(&existing); err != nil { + return ok, fmt.Errorf("error in decoding response %s", err) + } + exFields := collection.Map( + existing, + func(input tableFieldRes) string { return input.Name }, + ) + for _, fld := range tbm.FieldNames() { + if num := slices.Index(exFields, fld); num == -1 { + return ok, nil + } + } + + return true, nil +} + +func (tbm *TableManager) UpdateField( + tbl *client.Table, + field string, + updateSpec map[string]interface{}, +) (string, error) { + var empty string + fields, err := tbm.ListTableFields(tbl) + if err != nil { + return empty, err + } + updateOutput := F.Pipe3( + fields, + A.FindFirst(HasField(field)), + O.Map(ResToReqTableWithParams(updateSpec)), + O.Fold[tableFieldReq]( + onFieldDelReqFeedbackNone, + tbm.onFieldUpdateReqFeedbackSome, + ), + ) + + return updateOutput.Msg, updateOutput.Error +} + +func (tbm *TableManager) RemoveField( + tbl *client.Table, req string, +) (string, error) { + var empty string + fields, err := tbm.ListTableFields(tbl) + if err != nil { + return empty, err + } + delOutput := F.Pipe3( + fields, + A.FindFirst(HasField(req)), + O.Map(ResToReqTable), + O.Fold[tableFieldReq]( + onFieldDelReqFeedbackNone, + tbm.onFieldDelReqFeedbackSome, + ), + ) + + return delOutput.Msg, delOutput.Error +} + +func (tbm *TableManager) onFieldUpdateReqFeedbackSome( + req tableFieldReq, +) fieldsReqFeedback { + payloadResp := F.Pipe2( + req.Params, + J.Marshal, + E.Fold(onJSONPayloadError, onJSONPayloadSuccess), + ) + if payloadResp.Error != nil { + return fieldsReqFeedback{Error: payloadResp.Error} + } + resp := F.Pipe3( + tbm.TableFieldsChangeURL(req), + makeHTTPRequest("PATCH", bytes.NewBuffer(payloadResp.Payload)), + R.Map(httpapi.SetHeaderWithJWT(tbm.Token)), + readUpdateFieldsResp, + )(context.Background()) + + return F.Pipe1( + resp(), + E.Fold[error, tableFieldUpdateResponse, fieldsReqFeedback]( + onFieldsReqFeedbackError, + onFieldUpdateReqFeedbackSuccess, + ), + ) +} + +func (ont *TableManager) onFieldDelReqFeedbackSome( + req tableFieldReq, +) fieldsReqFeedback { + resp := F.Pipe3( + ont.TableFieldsChangeURL(req), + makeHTTPRequest("DELETE", nil), + R.Map(httpapi.SetHeaderWithJWT(ont.Token)), + readFieldDelResp, + )(context.Background()) + + return F.Pipe1( + resp(), + E.Fold[error, tableFieldDelResponse, fieldsReqFeedback]( + onFieldsReqFeedbackError, + onFieldDelReqFeedbackSuccess, + ), + ) +} diff --git a/internal/baserow/httpapi/api.go b/internal/baserow/httpapi/api.go new file mode 100644 index 00000000..7af54c6c --- /dev/null +++ b/internal/baserow/httpapi/api.go @@ -0,0 +1,80 @@ +package httpapi + +import ( + "fmt" + "io" + "net/http" + + F "github.com/IBM/fp-go/function" + H "github.com/IBM/fp-go/http/builder" + C "github.com/IBM/fp-go/http/content" + HD "github.com/IBM/fp-go/http/headers" + S "github.com/IBM/fp-go/string" +) + +var ( + WithJWT = F.Flow2( + S.Format[string]("JWT %s"), + H.WithAuthorization, + ) + WithToken = F.Flow2( + S.Format[string]("Token %s"), + H.WithAuthorization, + ) +) + +func SetHeaderWithToken(token string) func(*http.Request) *http.Request { + return func(req *http.Request) *http.Request { + req.Header = F.Pipe3( + H.Default, + H.WithContentType(C.Json), + H.WithHeader(HD.Accept)(C.Json), + WithToken(token), + ).GetHeaders() + + return req + } +} + +func SetHeaderWithJWT(jwt string) func(*http.Request) *http.Request { + return func(req *http.Request) *http.Request { + req.Header = F.Pipe3( + H.Default, + H.WithContentType(C.Json), + H.WithHeader(HD.Accept)(C.Json), + WithJWT(jwt), + ).GetHeaders() + + return req + } +} + +func CommonHeader(lreq *http.Request, token, format string) { + lreq.Header.Set("Content-Type", "application/json") + lreq.Header.Set("Accept", "application/json") + lreq.Header.Set("Authorization", fmt.Sprintf("%s %s", format, token)) +} + +func ReqToResponse(creq *http.Request) (*http.Response, error) { + client := &http.Client{} + uresp, err := client.Do(creq) + if err != nil { + return uresp, fmt.Errorf("error in making request %s", err) + } + if uresp.StatusCode != 200 { + cnt, err := io.ReadAll(uresp.Body) + if err != nil { + return uresp, fmt.Errorf( + "error in response and the reading the body %d %s", + uresp.StatusCode, + err, + ) + } + return uresp, fmt.Errorf( + "unexpected error response %d %s", + uresp.StatusCode, + string(cnt), + ) + } + return uresp, nil +} diff --git a/internal/baserow/ontology/load.go b/internal/baserow/ontology/load.go new file mode 100644 index 00000000..eb20cf03 --- /dev/null +++ b/internal/baserow/ontology/load.go @@ -0,0 +1,245 @@ +package ontology + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "os" + + "github.com/dictyBase/go-obograph/graph" + "github.com/dictyBase/modware-import/internal/baserow/client" + "github.com/dictyBase/modware-import/internal/baserow/httpapi" + "github.com/sirupsen/logrus" +) + +type LoadProperties struct { + File string + TableId int + Token string + Client *client.APIClient + Logger *logrus.Entry +} + +type termRowProperties struct { + Term graph.Term + Host string + Token string + TableId int +} + +type updateTermRowProperties struct { + *termRowProperties + RowId int32 +} + +type exisTermRowResp struct { + Exist bool + IsDeprecated bool + RowId int32 +} + +type ontologyRow struct { + Id int32 `json:"id"` + TermId string `json:"term_id"` + Name string `json:"name"` + IsObsolete bool `json:"is_obsolete"` +} + +type ontologyListRows struct { + Count int32 `json:"count"` + Next client.NullableString `json:"next"` + Previous client.NullableString `json:"previous"` + Results []*ontologyRow `json:"results"` +} + +func LoadNewOrUpdate(args *LoadProperties) error { + rdr, err := os.Open(args.File) + if err != nil { + return fmt.Errorf("error in opening file %s %s", args.File, err) + } + defer rdr.Close() + grph, err := graph.BuildGraph(rdr) + if err != nil { + return fmt.Errorf( + "error in building graph from file %s %s", + args.File, + err, + ) + } + for _, term := range grph.Terms() { + existResp, err := existTermRow(&termRowProperties{ + Term: term, + Host: args.Client.GetConfig().Host, + Token: args.Token, + TableId: args.TableId, + }) + if err != nil { + return err + } + if existResp.Exist { + if existResp.IsDeprecated == term.IsDeprecated() { + args.Logger.Debugf("term %s has no change", string(term.ID())) + continue + } + err = updateTermRow(&updateTermRowProperties{ + RowId: existResp.RowId, + termRowProperties: &termRowProperties{ + Term: term, + Host: args.Client.GetConfig().Host, + Token: args.Token, + TableId: args.TableId, + }, + }) + if err != nil { + return err + } + args.Logger.Infof("updated row with term %s", string(term.ID())) + continue + } + err = addTermRow(&termRowProperties{ + Term: term, + Host: args.Client.GetConfig().Host, + Token: args.Token, + TableId: args.TableId, + }) + if err != nil { + return err + } + args.Logger.Infof("add row with id %s", term.ID()) + } + return nil +} + +func LoadNew(args *LoadProperties) error { + rdr, err := os.Open(args.File) + if err != nil { + return fmt.Errorf("error in opening file %s %s", args.File, err) + } + defer rdr.Close() + grph, err := graph.BuildGraph(rdr) + if err != nil { + return fmt.Errorf( + "error in building graph from file %s %s", + args.File, + err, + ) + } + for _, term := range grph.Terms() { + err := addTermRow(&termRowProperties{ + Term: term, + Host: args.Client.GetConfig().Host, + Token: args.Token, + TableId: args.TableId, + }) + if err != nil { + return err + } + args.Logger.Infof("add row with id %s", term.ID()) + } + return nil +} + +func updateTermRow(args *updateTermRowProperties) error { + payload := map[string]interface{}{ + "is_obsolete": termStatus(args.Term), + } + jsonData, err := json.Marshal(payload) + if err != nil { + return fmt.Errorf("error in encoding body %s", err) + } + req, err := http.NewRequest( + "PATCH", + fmt.Sprintf( + "https://%s/api/database/rows/table/%d/%d/?user_field_names=true", + args.Host, + args.TableId, + args.RowId, + ), + bytes.NewBuffer(jsonData), + ) + if err != nil { + return fmt.Errorf("error in creating requst %s", err) + } + httpapi.CommonHeader(req, args.Token, "Token") + res, err := httpapi.ReqToResponse(req) + if err != nil { + return err + } + defer res.Body.Close() + + return nil +} + +func existTermRow(args *termRowProperties) (*exisTermRowResp, error) { + term := string(args.Term.ID()) + req, err := http.NewRequest( + "GET", + fmt.Sprintf( + "https://%s/api/database/rows/table/%d/?user_field_names=true&size=1&search=%s", + args.Host, + args.TableId, + term, + ), nil, + ) + if err != nil { + return nil, fmt.Errorf("error in creating requst %s", err) + } + httpapi.CommonHeader(req, args.Token, "Token") + res, err := httpapi.ReqToResponse(req) + if err != nil { + return nil, err + } + defer res.Body.Close() + rowsResp := &ontologyListRows{} + if err := json.NewDecoder(res.Body).Decode(rowsResp); err != nil { + return nil, fmt.Errorf("error in decoding json response %s", err) + } + existResp := &exisTermRowResp{Exist: false} + if rowsResp.Count > 0 { + existResp.Exist = true + existResp.IsDeprecated = rowsResp.Results[0].IsObsolete + existResp.RowId = rowsResp.Results[0].Id + } + return existResp, nil +} + +func addTermRow(args *termRowProperties) error { + term := args.Term + payload := map[string]interface{}{ + "term_id": string(term.ID()), + "name": term.Label(), + "is_obsolete": termStatus(term), + } + jsonData, err := json.Marshal(payload) + if err != nil { + return fmt.Errorf("error in encoding body %s", err) + } + req, err := http.NewRequest( + "POST", + fmt.Sprintf( + "https://%s/api/database/rows/table/%d/?user_field_names=true", + args.Host, + args.TableId, + ), + bytes.NewBuffer(jsonData), + ) + if err != nil { + return fmt.Errorf("error in creating request %s ", err) + } + httpapi.CommonHeader(req, args.Token, "Token") + res, err := httpapi.ReqToResponse(req) + if err != nil { + return err + } + defer res.Body.Close() + + return nil +} + +func termStatus(term graph.Term) string { + if term.IsDeprecated() { + return "true" + } + return "false" +}