graphql-engine/cli/internal/metadataobject/actions/actions.go
Aravind K P 3d7effbcc1 cli: refactor metadataobject.WriteDiff to use std error
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/6389
GitOrigin-RevId: 731b74ae5bb1ad0479c141f1e66795d9fd21e9e3
2022-10-20 06:58:52 +00:00

488 lines
17 KiB
Go

package actions
import (
"bytes"
"fmt"
"io/ioutil"
"path/filepath"
"github.com/hasura/graphql-engine/cli/v2/internal/metadataobject"
"github.com/hasura/graphql-engine/cli/v2"
"github.com/hasura/graphql-engine/cli/v2/internal/cliext"
cliextension "github.com/hasura/graphql-engine/cli/v2/internal/metadataobject/actions/cli_extension"
"github.com/hasura/graphql-engine/cli/v2/internal/metadataobject/actions/editor"
"github.com/hasura/graphql-engine/cli/v2/internal/metadataobject/actions/types"
"github.com/hasura/graphql-engine/cli/v2/util"
"github.com/hasura/graphql-engine/cli/v2/version"
"github.com/sirupsen/logrus"
"gopkg.in/yaml.v3"
)
const (
graphqlFileName = "actions.graphql"
)
type ActionConfig struct {
MetadataDir string
ActionConfig *types.ActionExecutionConfig
serverFeatureFlags *version.ServerFeatureFlags
cliExtensionConfig *cliextension.Config
ensureCliExt func() error
cleanupCliExt func()
logger *logrus.Logger
}
func New(ec *cli.ExecutionContext, baseDir string) *ActionConfig {
cfg := &ActionConfig{
MetadataDir: baseDir,
ActionConfig: ec.Config.ActionConfig,
serverFeatureFlags: ec.Version.ServerFeatureFlags,
logger: ec.Logger,
cliExtensionConfig: cliextension.NewCLIExtensionConfig(&ec.CliExtDestinationBinPath, ec.Logger),
ensureCliExt: func() error {
return cliext.Setup(ec)
},
cleanupCliExt: func() {
cliext.Cleanup(ec)
},
}
return cfg
}
func (a *ActionConfig) Create(name string, introSchema interface{}, deriveFrom string) error {
err := a.ensureCliExt()
defer a.cleanupCliExt()
if err != nil {
return err
}
// Read the content of graphql file
graphqlFileContent, err := a.GetActionsGraphQLFileContent()
if err != nil {
return fmt.Errorf("error in reading %s file: %w", graphqlFileName, err)
}
// Read actions.yaml
oldAction, err := a.GetActionsFileContent()
if err != nil {
return fmt.Errorf("error in reading %s file: %w", a.Filename(), err)
}
// check if action already present
for _, currAction := range oldAction.Actions {
if currAction.Name == name {
return fmt.Errorf("action %s already exists in %s", name, graphqlFileName)
}
}
var defaultSDL string
if introSchema == nil {
defaultSDL = `type Mutation {
# Define your action as a mutation here
` + name + ` (arg1: SampleInput!): SampleOutput
}
type SampleOutput {
accessToken: String!
}
input SampleInput {
username: String!
password: String!
}
`
} else {
sdlToReq := types.SDLToRequest{
Derive: types.DerivePayload{
IntrospectionSchema: introSchema,
Operation: deriveFrom,
ActionName: name,
},
}
sdlToResp, err := a.cliExtensionConfig.ConvertMetadataToSDL(sdlToReq)
if err != nil {
return fmt.Errorf("error in converting metadata to sdl: %w", err)
}
defaultSDL = sdlToResp.SDL.Complete
}
graphqlFileContent = defaultSDL + "\n" + graphqlFileContent
data, err := editor.CaptureInputFromEditor(editor.GetPreferredEditorFromEnvironment, graphqlFileContent, "graphql")
if err != nil {
return fmt.Errorf("error in getting input from editor: %w", err)
}
sdlFromReq := types.SDLFromRequest{
SDL: types.SDLPayload{
Complete: string(data),
},
}
sdlFromResp, err := a.cliExtensionConfig.ConvertSDLToMetadata(sdlFromReq)
if err != nil {
return fmt.Errorf("error in converting sdl to metadata: %w", err)
}
currentActionNames := make([]string, 0)
for actionIndex, action := range sdlFromResp.Actions {
for _, currAction := range currentActionNames {
if currAction == action.Name {
return fmt.Errorf("action %s already exists in %s", action.Name, graphqlFileName)
}
}
currentActionNames = append(currentActionNames, action.Name)
for oldActionIndex, oldActionObj := range oldAction.Actions {
if action.Name == oldActionObj.Name {
sdlFromResp.Actions[actionIndex].Permissions = oldAction.Actions[oldActionIndex].Permissions
sdlFromResp.Actions[actionIndex].Definition.Timeout = oldAction.Actions[oldActionIndex].Definition.Timeout
sdlFromResp.Actions[actionIndex].Definition.Kind = oldAction.Actions[oldActionIndex].Definition.Kind
sdlFromResp.Actions[actionIndex].Definition.Type = oldAction.Actions[oldActionIndex].Definition.Type
sdlFromResp.Actions[actionIndex].Definition.Handler = oldAction.Actions[oldActionIndex].Definition.Handler
sdlFromResp.Actions[actionIndex].Definition.ForwardClientHeaders = oldAction.Actions[oldActionIndex].Definition.ForwardClientHeaders
sdlFromResp.Actions[actionIndex].Definition.Headers = oldAction.Actions[oldActionIndex].Definition.Headers
break
}
}
// Set kind and handler for action definition
if sdlFromResp.Actions[actionIndex].Definition.Kind == "" {
sdlFromResp.Actions[actionIndex].Definition.Kind = a.ActionConfig.Kind
}
if sdlFromResp.Actions[actionIndex].Definition.Handler == "" {
sdlFromResp.Actions[actionIndex].Definition.Handler = a.ActionConfig.HandlerWebhookBaseURL + "/" + action.Name
}
}
for customTypeIndex, customType := range sdlFromResp.Types.Enums {
for oldTypeObjIndex, oldTypeObj := range oldAction.CustomTypes.Enums {
if customType.Name == oldTypeObj.Name {
sdlFromResp.Types.Enums[customTypeIndex].Description = oldAction.CustomTypes.Enums[oldTypeObjIndex].Description
sdlFromResp.Types.Enums[customTypeIndex].Relationships = oldAction.CustomTypes.Enums[oldTypeObjIndex].Relationships
break
}
}
}
for customTypeIndex, customType := range sdlFromResp.Types.InputObjects {
for oldTypeObjIndex, oldTypeObj := range oldAction.CustomTypes.InputObjects {
if customType.Name == oldTypeObj.Name {
sdlFromResp.Types.InputObjects[customTypeIndex].Description = oldAction.CustomTypes.InputObjects[oldTypeObjIndex].Description
sdlFromResp.Types.InputObjects[customTypeIndex].Relationships = oldAction.CustomTypes.InputObjects[oldTypeObjIndex].Relationships
break
}
}
}
for customTypeIndex, customType := range sdlFromResp.Types.Objects {
for oldTypeObjIndex, oldTypeObj := range oldAction.CustomTypes.Objects {
if customType.Name == oldTypeObj.Name {
sdlFromResp.Types.Objects[customTypeIndex].Description = oldAction.CustomTypes.Objects[oldTypeObjIndex].Description
sdlFromResp.Types.Objects[customTypeIndex].Relationships = oldAction.CustomTypes.Objects[oldTypeObjIndex].Relationships
break
}
}
}
for customTypeIndex, customType := range sdlFromResp.Types.Scalars {
for oldTypeObjIndex, oldTypeObj := range oldAction.CustomTypes.Scalars {
if customType.Name == oldTypeObj.Name {
sdlFromResp.Types.Scalars[customTypeIndex].Description = oldAction.CustomTypes.Scalars[oldTypeObjIndex].Description
sdlFromResp.Types.Scalars[customTypeIndex].Relationships = oldAction.CustomTypes.Scalars[oldTypeObjIndex].Relationships
break
}
}
}
var common types.Common
common.Actions = sdlFromResp.Actions
common.CustomTypes = sdlFromResp.Types
common.SetExportDefault()
// write actions.yaml
commonByt, err := yaml.Marshal(common)
if err != nil {
return fmt.Errorf("error in marshalling common: %w", err)
}
err = ioutil.WriteFile(filepath.Join(a.MetadataDir, a.Filename()), commonByt, 0644)
if err != nil {
return fmt.Errorf("error in writing %s file: %w", a.Filename(), err)
}
err = ioutil.WriteFile(filepath.Join(a.MetadataDir, graphqlFileName), data, 0644)
if err != nil {
return fmt.Errorf("error in writing %s file: %w", graphqlFileName, err)
}
return nil
}
func (a *ActionConfig) Codegen(name string, derivePld types.DerivePayload) error {
err := a.ensureCliExt()
defer a.cleanupCliExt()
if err != nil {
return err
}
graphqlFileContent, err := a.GetActionsGraphQLFileContent()
if err != nil {
return fmt.Errorf("error in reading %s file: %w", graphqlFileName, err)
}
data := types.ActionsCodegenRequest{
ActionName: name,
SDL: types.SDLPayload{
Complete: graphqlFileContent,
},
CodegenConfig: a.ActionConfig.Codegen,
Derive: derivePld,
}
if a.ActionConfig.Codegen.URI == "" {
data.CodegenConfig.URI = a.getActionsCodegenURI(data.CodegenConfig.Framework)
}
resp, err := a.cliExtensionConfig.GetActionsCodegen(data)
if err != nil {
return fmt.Errorf("error in getting codegen for action %s: %w", data.ActionName, err)
}
for _, file := range resp.Files {
err = ioutil.WriteFile(filepath.Join(a.ActionConfig.Codegen.OutputDir, file.Name), []byte(file.Content), 0644)
if err != nil {
return fmt.Errorf("error in writing codegen file: %w", err)
}
}
return nil
}
func (a *ActionConfig) Validate() error {
return nil
}
func (a *ActionConfig) CreateFiles() error {
var common types.Common
data, err := yaml.Marshal(common)
if err != nil {
return err
}
err = ioutil.WriteFile(filepath.Join(a.MetadataDir, a.Filename()), data, 0644)
if err != nil {
return err
}
graphqQLData := []byte(``)
err = ioutil.WriteFile(filepath.Join(a.MetadataDir, graphqlFileName), graphqQLData, 0644)
if err != nil {
return err
}
return nil
}
func (a *ActionConfig) Build() (map[string]interface{}, error) {
if !a.serverFeatureFlags.HasAction {
_, err := a.GetActionsFileContent()
if err == nil {
a.logger.WithField("metadata_plugin", "actions").Warnf("Skipping building %s", a.Filename())
}
_, err = a.GetActionsGraphQLFileContent()
if err == nil {
a.logger.WithField("metadata_plugin", "actions").Warnf("Skipping building %s", graphqlFileName)
}
return nil, nil
}
err := a.ensureCliExt()
defer a.cleanupCliExt()
if err != nil {
return nil, a.error(err)
}
// Read actions.graphql
graphqlFileContent, err := a.GetActionsGraphQLFileContent()
if err != nil {
return nil, a.error(fmt.Errorf("error in reading %s file: %w", graphqlFileName, err))
}
sdlFromReq := types.SDLFromRequest{
SDL: types.SDLPayload{
Complete: graphqlFileContent,
},
}
sdlFromResp, err := a.cliExtensionConfig.ConvertSDLToMetadata(sdlFromReq)
if err != nil {
return nil, a.error(fmt.Errorf("error in converting sdl to metadata: %w", err))
}
// Read actions.yaml
oldAction, err := a.GetActionsFileContent()
if err != nil {
return nil, a.error(fmt.Errorf("error in reading %s: %w", a.Filename(), err))
}
for actionIndex, action := range oldAction.Actions {
var isFound bool
for newActionIndex, newActionObj := range sdlFromResp.Actions {
if action.Name == newActionObj.Name {
isFound = true
sdlFromResp.Actions[newActionIndex].Permissions = oldAction.Actions[actionIndex].Permissions
sdlFromResp.Actions[newActionIndex].Comment = oldAction.Actions[actionIndex].Comment
sdlFromResp.Actions[newActionIndex].Definition.Timeout = oldAction.Actions[actionIndex].Definition.Timeout
sdlFromResp.Actions[newActionIndex].Definition.Kind = oldAction.Actions[actionIndex].Definition.Kind
sdlFromResp.Actions[newActionIndex].Definition.Handler = oldAction.Actions[actionIndex].Definition.Handler
sdlFromResp.Actions[newActionIndex].Definition.ForwardClientHeaders = oldAction.Actions[actionIndex].Definition.ForwardClientHeaders
sdlFromResp.Actions[newActionIndex].Definition.Headers = oldAction.Actions[actionIndex].Definition.Headers
sdlFromResp.Actions[newActionIndex].Definition.RequestTransform = oldAction.Actions[actionIndex].Definition.RequestTransform
break
}
}
if !isFound {
return nil, a.error(fmt.Errorf("action %s is not present in %s", action.Name, graphqlFileName))
}
}
for customTypeIndex, customType := range oldAction.CustomTypes.Enums {
var isFound bool
for newTypeObjIndex, newTypeObj := range sdlFromResp.Types.Enums {
if customType.Name == newTypeObj.Name {
isFound = true
sdlFromResp.Types.Enums[newTypeObjIndex].Description = oldAction.CustomTypes.Enums[customTypeIndex].Description
sdlFromResp.Types.Enums[newTypeObjIndex].Relationships = oldAction.CustomTypes.Enums[customTypeIndex].Relationships
break
}
}
if !isFound {
return nil, a.error(fmt.Errorf("custom type %s is not present in %s", customType.Name, graphqlFileName))
}
}
for customTypeIndex, customType := range oldAction.CustomTypes.InputObjects {
var isFound bool
for newTypeObjIndex, newTypeObj := range sdlFromResp.Types.InputObjects {
if customType.Name == newTypeObj.Name {
isFound = true
sdlFromResp.Types.InputObjects[newTypeObjIndex].Description = oldAction.CustomTypes.InputObjects[customTypeIndex].Description
sdlFromResp.Types.InputObjects[newTypeObjIndex].Relationships = oldAction.CustomTypes.InputObjects[customTypeIndex].Relationships
break
}
}
if !isFound {
return nil, a.error(fmt.Errorf("custom type %s is not present in %s", customType.Name, graphqlFileName))
}
}
for customTypeIndex, customType := range oldAction.CustomTypes.Objects {
var isFound bool
for newTypeObjIndex, newTypeObj := range sdlFromResp.Types.Objects {
if customType.Name == newTypeObj.Name {
isFound = true
sdlFromResp.Types.Objects[newTypeObjIndex].Description = oldAction.CustomTypes.Objects[customTypeIndex].Description
sdlFromResp.Types.Objects[newTypeObjIndex].Relationships = oldAction.CustomTypes.Objects[customTypeIndex].Relationships
break
}
}
if !isFound {
return nil, a.error(fmt.Errorf("custom type %s is not present in %s", customType.Name, graphqlFileName))
}
}
for customTypeIndex, customType := range oldAction.CustomTypes.Scalars {
var isFound bool
for newTypeObjIndex, newTypeObj := range sdlFromResp.Types.Scalars {
if customType.Name == newTypeObj.Name {
isFound = true
sdlFromResp.Types.Scalars[newTypeObjIndex].Description = oldAction.CustomTypes.Scalars[customTypeIndex].Description
sdlFromResp.Types.Scalars[newTypeObjIndex].Relationships = oldAction.CustomTypes.Scalars[customTypeIndex].Relationships
break
}
}
if !isFound {
return nil, a.error(fmt.Errorf("custom type %s is not present in %s", customType.Name, graphqlFileName))
}
}
metadata := map[string]interface{}{}
if len(sdlFromResp.Actions) != 0 {
metadata[metadataobject.ActionsKey] = sdlFromResp.Actions
}
customTypesLen := len(sdlFromResp.Types.Enums) + len(sdlFromResp.Types.InputObjects) + len(sdlFromResp.Types.Objects) + len(sdlFromResp.Types.Scalars)
if customTypesLen != 0 {
metadata[metadataobject.CustomTypesKey] = sdlFromResp.Types
}
return metadata, nil
}
func (a *ActionConfig) Export(metadata map[string]yaml.Node) (map[string][]byte, error) {
if !a.serverFeatureFlags.HasAction {
a.logger.Debugf("Skipping creating %s and %s", a.Filename(), graphqlFileName)
return make(map[string][]byte), nil
}
err := a.ensureCliExt()
defer a.cleanupCliExt()
if err != nil {
return nil, a.error(err)
}
actions := map[string]yaml.Node{}
if v, ok := metadata[metadataobject.ActionsKey]; ok {
actions[metadataobject.ActionsKey] = v
}
if v, ok := metadata[metadataobject.CustomTypesKey]; ok {
actions[metadataobject.CustomTypesKey] = v
}
ymlByt := new(bytes.Buffer)
if err := metadataobject.GetEncoder(ymlByt).Encode(actions); err != nil {
return nil, a.error(fmt.Errorf("error in marshalling actions, custom_types from metadata: %w", err))
}
var common types.Common
err = yaml.NewDecoder(ymlByt).Decode(&common)
if err != nil {
return nil, a.error(fmt.Errorf("error in unmarshal to common: %w", err))
}
var sdlToReq types.SDLToRequest
sdlToReq.Types = common.CustomTypes
sdlToReq.Actions = common.Actions
sdlToResp, err := a.cliExtensionConfig.ConvertMetadataToSDL(sdlToReq)
if err != nil {
return nil, a.error(fmt.Errorf("error in converting metadata to sdl: %w", err))
}
common.SetExportDefault()
commonByt := new(bytes.Buffer)
if err = metadataobject.GetEncoder(commonByt).Encode(common); err != nil {
return nil, a.error(fmt.Errorf("error in marshaling common: %w", err))
}
return map[string][]byte{
filepath.ToSlash(filepath.Join(a.MetadataDir, a.Filename())): commonByt.Bytes(),
filepath.ToSlash(filepath.Join(a.MetadataDir, graphqlFileName)): []byte(sdlToResp.SDL.Complete),
}, nil
}
func (a *ActionConfig) Key() string {
return metadataobject.ActionsKey
}
func (a *ActionConfig) Filename() string {
return "actions.yaml"
}
func (a *ActionConfig) GetFiles() ([]string, error) {
rootFile := filepath.Join(a.BaseDirectory(), a.Filename())
files, err := metadataobject.DefaultGetFiles(rootFile)
if err != nil {
return nil, a.error(err)
}
files = append(files, filepath.Join(a.BaseDirectory(), graphqlFileName))
return files, nil
}
func (a *ActionConfig) WriteDiff(opts metadataobject.WriteDiffOpts) error {
err := metadataobject.DefaultWriteDiff(metadataobject.DefaultWriteDiffOpts{From: a, WriteDiffOpts: opts})
if err != nil {
return a.error(err)
}
return nil
}
func (a *ActionConfig) BaseDirectory() string {
return a.MetadataDir
}
func (a *ActionConfig) GetActionsFileContent() (content types.Common, err error) {
commonByt, err := metadataobject.ReadMetadataFile(filepath.Join(a.MetadataDir, a.Filename()))
if err != nil {
return
}
err = yaml.Unmarshal(commonByt, &content)
return
}
func (a *ActionConfig) GetActionsGraphQLFileContent() (sdl string, err error) {
commonByt, err := metadataobject.ReadMetadataFile(filepath.Join(a.MetadataDir, graphqlFileName))
if err != nil {
return
}
sdl = string(commonByt)
return
}
func (a *ActionConfig) getActionsCodegenURI(framework string) string {
return fmt.Sprintf(`https://raw.githubusercontent.com/%s/master/%s/actions-codegen.js`, util.ActionsCodegenOrg, framework)
}
func (a *ActionConfig) error(err error, additionalContext ...string) metadataobject.ErrParsingMetadataObject {
return metadataobject.NewErrParsingMetadataObject(a, err, additionalContext...)
}