diff --git a/go/README.md b/go/README.md index 03096b1..17c1706 100644 --- a/go/README.md +++ b/go/README.md @@ -2,14 +2,12 @@ How to start: 1. Change the current directory to the directory with this README file. -2. Create the `env.json` file from the `env.json.template` and place it in the same directory: +2. Create the `../.env` file from the `../.env.template` (see the repository root for file `.env.template`) ```sh -cp env/env.json.template env/env.json +cp ../.env.template ../.env ``` -3. Fill the `env/env.json` file - * The fields in the `server` object correspond to the fields of the same object from the [cloudbeaver.conf](https://dbeaver.com/docs/cloudbeaver/Server-configuration/#main-server-configuration). - * `apiToken` is the [API token](https://github.com/dbeaver/cloudbeaver/wiki/Generate-API-access-token). -5. You are ready to Go! +3. Fill the environment variables in the `.env` file. +4. You are ready to Go! ```sh go build && ./go ``` diff --git a/go/api/client.go b/go/api/client.go index a41daad..21a6ac9 100644 --- a/go/api/client.go +++ b/go/api/client.go @@ -5,66 +5,17 @@ import ( "errors" "fmt" "log/slog" + "os" "time" "github.com/dbeaver/cloudbeaver-graphql-examples/go/graphql" "github.com/dbeaver/cloudbeaver-graphql-examples/go/lib" ) -// Queries and mutations -const ( - authQuery = ` -query authLogin($token: String!) { - authLogin(provider: "token", credentials: { token: $token }) { - userTokens { - userId - } - authStatus - } -} -` - - createTeamQuery = ` -query createTeam($teamId: ID!) { - createTeam(teamId: $teamId) { - teamId - } -} -` - - deleteTeamQuery = ` -query deleteTeam($teamId: ID!, $force: Boolean) { - deleteTeam(teamId: $teamId, force: $force) -} -` - - createProjectMutation = ` -mutation RmCreateProject($projectName: String!) { - rmCreateProject(projectName: $projectName) { - id - } -} -` - - deleteProjectMutation = ` -mutation RmDeleteProject($projectId: ID!) { - rmDeleteProject(projectId: $projectId) -} -` - addProjectPermissionsMutation = ` -mutation addProjectsPermissions($projectIds: [ID!]!, $subjectIds: [ID!]!, $permissions: [String!]!) { - rmAddProjectsPermissions( - projectIds: $projectIds - subjectIds: $subjectIds - permissions: $permissions - ) -} -` -) - type Client struct { - GraphQLClient graphql.Client - Endpoint string + GraphQLClient graphql.Client + Endpoint string + OperationsPath string } func (client Client) sendRequest(operationName, query string, variables graphql.Object) (json.RawMessage, error) { @@ -90,42 +41,70 @@ func (client Client) sendRequestDiscardingData(operationName, query string, vari return err } +func (client Client) readOperationText(operationName string) (string, error) { + path := client.OperationsPath + "/" + operationName + ".gql" + bytes, err := os.ReadFile(path) + if err != nil { + return "", lib.WrapError(fmt.Sprintf("unable to read operation file %s", path), err) + } + return string(bytes), nil +} + func (client Client) Auth(token string) error { + query, err := client.readOperationText("auth") + if err != nil { + return err + } variables := map[string]any{ "token": token, } - return client.sendRequestDiscardingData("auth", authQuery, variables) + return client.sendRequestDiscardingData("auth", query, variables) } func (client Client) CreateTeam(teamId string) error { + query, err := client.readOperationText("create_team") + if err != nil { + return err + } variables := map[string]any{ "teamId": teamId, } return client.sendRequestDiscardingData( fmt.Sprintf("create team '%s'", teamId), - createTeamQuery, - variables) + query, + variables, + ) } func (client Client) DeleteTeam(teamId string, force bool) error { + query, err := client.readOperationText("delete_team") + if err != nil { + return err + } variables := map[string]any{ "teamId": teamId, "force": force, } return client.sendRequestDiscardingData( fmt.Sprintf("delete team '%s'", teamId), - deleteTeamQuery, - variables) + query, + variables, + ) } func (client Client) CreateProject(projectName string) (id string, err error) { + query, err := client.readOperationText("create_project") + if err != nil { + return "", err + } variables := map[string]any{ "projectName": projectName, } rawData, err := client.sendRequest( fmt.Sprintf("create project with name '%s'", projectName), - createProjectMutation, - variables) + query, + variables, + ) if err != nil { return id, err } @@ -138,18 +117,26 @@ func (client Client) CreateProject(projectName string) (id string, err error) { } func (client Client) DeleteProject(projectId string) error { + query, err := client.readOperationText("delete_project") + if err != nil { + return err + } variables := map[string]any{ "projectId": projectId, } return client.sendRequestDiscardingData( fmt.Sprintf("delete project with id '%s'", projectId), - deleteProjectMutation, + query, variables, ) } // subjectIds: Ids of teams or individual users func (client Client) AddProjectAccess(projectId string, subjectIds ...string) error { + query, err := client.readOperationText("add_project_permissions") + if err != nil { + return err + } variables := map[string]any{ "projectIds": [1]string{projectId}, "subjectIds": subjectIds, @@ -160,7 +147,7 @@ func (client Client) AddProjectAccess(projectId string, subjectIds ...string) er } return client.sendRequestDiscardingData( fmt.Sprintf("grant subjects %s access to project with id '%s'", subjectIds, projectId), - addProjectPermissionsMutation, + query, variables, ) } diff --git a/go/env/env.go b/go/env/env.go deleted file mode 100644 index c95d3dc..0000000 --- a/go/env/env.go +++ /dev/null @@ -1,41 +0,0 @@ -package env - -import ( - "encoding/json" - "os" - "runtime" - - "github.com/dbeaver/cloudbeaver-graphql-examples/go/lib" -) - -type Env struct { - ServerInfo ServerInfo `json:"server"` - Token string `json:"apiToken"` -} - -type ServerInfo struct { - ServerURL string `json:"serverURL"` - ServiceURI string `json:"serviceURI"` -} - -func Read() (Env, error) { - env := Env{} - bytes, err := os.ReadFile("env/env.json") - if err != nil { - return env, err - } - err = json.Unmarshal(bytes, &env) - if err != nil { - err = lib.WrapError("error while unmarshalling the env file", err) - } - return env, err -} - -func (env *Env) GraphqlEndpoint() string { - return env.ServerInfo.ServerURL + "/" + env.ServerInfo.ServiceURI + "/gql" -} - -func (env *Env) PurgeToken() { - env.Token = "" - runtime.GC() -} diff --git a/go/env/env.json.template b/go/env/env.json.template deleted file mode 100644 index fe1e053..0000000 --- a/go/env/env.json.template +++ /dev/null @@ -1,7 +0,0 @@ -{ - "server": { - "serverURL": "", - "serviceURI": "api" - }, - "apiToken": "" -} diff --git a/go/graphql/client.go b/go/graphql/client.go index d978a34..70133c4 100644 --- a/go/graphql/client.go +++ b/go/graphql/client.go @@ -4,7 +4,6 @@ import ( "bytes" "encoding/json" "io" - "log/slog" "net/http" "github.com/dbeaver/cloudbeaver-graphql-examples/go/lib" @@ -23,7 +22,7 @@ func (client Client) Execute(endpoint string, request Request) (Response, error) if err != nil { return Response{}, lib.WrapError("unable to execute POST request", err) } - defer closeOrWarn(httpResponse.Body) + defer lib.CloseOrWarn(httpResponse.Body) rawResponseBody, err := io.ReadAll(httpResponse.Body) if err != nil { return Response{}, lib.WrapError("unable to read GraphQL response body", err) @@ -34,9 +33,3 @@ func (client Client) Execute(endpoint string, request Request) (Response, error) } return response, err } - -func closeOrWarn(closer io.Closer) { - if err := closer.Close(); err != nil { - slog.Warn("error while closing a Closer: " + err.Error()) - } -} diff --git a/go/lib/lib.go b/go/lib/lib.go index 50ccae3..e8e90d6 100644 --- a/go/lib/lib.go +++ b/go/lib/lib.go @@ -1,7 +1,17 @@ package lib -import "fmt" +import ( + "fmt" + "io" + "log/slog" +) func WrapError(errorMessage string, original error) error { return fmt.Errorf("%s\n\tCaused by: %w", errorMessage, original) } + +func CloseOrWarn(closer io.Closer) { + if err := closer.Close(); err != nil { + slog.Warn("error while closing a Closer: " + err.Error()) + } +} diff --git a/go/main.go b/go/main.go index 73c33f6..da5c3d1 100644 --- a/go/main.go +++ b/go/main.go @@ -1,13 +1,16 @@ package main import ( + "bufio" + "flag" + "fmt" "log/slog" "net/http" "net/http/cookiejar" "os" + "strings" "github.com/dbeaver/cloudbeaver-graphql-examples/go/api" - "github.com/dbeaver/cloudbeaver-graphql-examples/go/env" "github.com/dbeaver/cloudbeaver-graphql-examples/go/graphql" "github.com/dbeaver/cloudbeaver-graphql-examples/go/lib" ) @@ -27,15 +30,17 @@ func main() { func main0() error { // Instantiate a client - env, err := env.Read() + envFlag := flag.String("env", "../.env", "Path to the .env file") + operationsFlag := flag.String("operations", "../operations", "Path to the folder with GraphQL operations") + flag.Parse() + env, err := readEnv(*envFlag) if err != nil { return lib.WrapError("error while reading variables", err) } - apiClient := initClient(env.GraphqlEndpoint()) + apiClient := initClient(env.serverURL+"/"+env.serviceURI+"/gql", *operationsFlag) // Auth - err = apiClient.Auth(env.Token) - env.PurgeToken() + err = apiClient.Auth(env.apiToken) if err != nil { return err } @@ -67,14 +72,49 @@ func main0() error { return nil } -func initClient(endpoint string) api.Client { +type env struct { + apiToken string + serverURL string + serviceURI string +} + +func readEnv(envFilePath string) (env, error) { + env := env{} + file, err := os.Open(envFilePath) + if err != nil { + return env, lib.WrapError("error while opening the env file", err) + } + defer lib.CloseOrWarn(file) + scanner := bufio.NewScanner(file) + for scanner.Scan() { + before, after, found := strings.Cut(scanner.Text(), "=") + if !found { + continue + } + before = strings.TrimSpace(before) + after = strings.TrimSpace(after) + switch before { + case "api_token": + env.apiToken = after + case "server_url": + env.serverURL = after + case "service_uri": + env.serviceURI = after + default: + slog.Warn(fmt.Sprintf("unknown env variable: %s", before)) + } + } + return env, err +} + +func initClient(endpoint string, operationsPath string) api.Client { cookieJar, err := cookiejar.New(nil) if err != nil { // Invariant: the method that creates cookie jar with no options never returns non-nil err panic("encountered error while creating a cookie jar! " + err.Error()) } graphQLClient := graphql.Client{HttpClient: &http.Client{Jar: cookieJar}} - return api.Client{GraphQLClient: graphQLClient, Endpoint: endpoint} + return api.Client{GraphQLClient: graphQLClient, Endpoint: endpoint, OperationsPath: operationsPath} } func cleanup(callDescription string, apiCall func() error) { diff --git a/operations/add_project_permissions.gql b/operations/add_project_permissions.gql new file mode 100644 index 0000000..963c457 --- /dev/null +++ b/operations/add_project_permissions.gql @@ -0,0 +1,7 @@ +mutation addProjectsPermissions($projectIds: [ID!]!, $subjectIds: [ID!]!, $permissions: [String!]!) { + rmAddProjectsPermissions( + projectIds: $projectIds + subjectIds: $subjectIds + permissions: $permissions + ) +} diff --git a/operations/create_project.gql b/operations/create_project.gql new file mode 100644 index 0000000..47d4387 --- /dev/null +++ b/operations/create_project.gql @@ -0,0 +1,5 @@ +mutation RmCreateProject($projectName: String!) { + rmCreateProject(projectName: $projectName) { + id + } +} diff --git a/operations/delete_project.gql b/operations/delete_project.gql new file mode 100644 index 0000000..8fb7af1 --- /dev/null +++ b/operations/delete_project.gql @@ -0,0 +1,3 @@ +mutation RmDeleteProject($projectId: ID!) { + rmDeleteProject(projectId: $projectId) +}