Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: partial schema update #13

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ Given a schema specified by `schema_file`, to update/create AppSync schema Or/A
* `schema_file`: *Optional.* .grapqh schema File provided by an output of a task, if you didn't specify `resolvers_file` this field is *Required.*.

* `resolvers_file`: *Optional.* .yml resolver String provided by an output of a task, if you didn't specify `schema_file` this field is *Required.*.
.

* `partial_update`: *Optional.* Boolean to specify if to update the entire schema or only fields and types related to the fields and types resolved by the resolvers defined in `resolvers_file`. DEFAULT: `false`. If this is true `resolvers_file` is **Required**.

## Example Configuration

Expand Down Expand Up @@ -81,6 +82,7 @@ resource:
| region_name | No | eu-west-1 | AWS region DEFAULT: eu-west-1 |
| ci | No | github | DEFAULT: github |
| schema_file | No | workspace/schema.graphql | |
| partial_update | No | false | |
| resolvers_file | No | workspace/resolvers.yml | |

## Example Configuration
Expand All @@ -96,6 +98,7 @@ jobs:
with:
schema_file: "workspace/schema.graphql"
resolvers_file: "workspace/resolvers.yml"
partial_update: "false"
access_key_id: {YOUR_ACCESS_KEY_ID}
secret_access_key: {YOUR_SECRET_ACCESS_KEY}
session_token: {YOUR_SESSION_TOKEN}
Expand Down
3 changes: 3 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ inputs:
required: false
resolvers_file:
required: false
partial_update:
required: false
access_key_id:
required: true
secret_access_key:
Expand All @@ -28,6 +30,7 @@ runs:
- ${{ inputs.ci}}
- ${{ inputs.resolvers_file}}
- ${{ inputs.schema_file}}
- ${{ inputs.partial_update}}
- ${{ inputs.access_key_id}}
- ${{ inputs.secret_access_key}}
- ${{ inputs.session_token}}
Expand Down
34 changes: 30 additions & 4 deletions appSyncClient.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ type AppSync interface {
CreateOrUpdateResolvers(apiID string, resolversFile []byte, logger *log.Logger) (Statistics, Statistics, error)
StartSchemaCreationOrUpdate(apiID string, schema []byte) error
GetSchemaCreationStatus(apiID string) (string, string, error)
StartPartialSchemaCreationOrUpdate(apiID string, schema []byte, resolverFile []byte) error
}

type appSyncClient struct {
Expand Down Expand Up @@ -140,7 +141,7 @@ func (client *appSyncClient) CreateOrUpdateResolvers(apiID string, resolversFile
FunctionVersion: aws.String("2018-05-29"),
})
if err != nil {
logger.Println(fmt.Sprintf("Function %s failed to update: %s", function.Name, err))
logger.Printf("Function %s failed to update: %s\n", function.Name, err)
functionStatistics.FailedToUpdate++
} else {
functionStatistics.Updated++
Expand All @@ -157,7 +158,7 @@ func (client *appSyncClient) CreateOrUpdateResolvers(apiID string, resolversFile
})

if err != nil {
logger.Println(fmt.Sprintf("Function %s failed to create: %s", function.Name, err))
logger.Printf("Function %s failed to create: %s\n", function.Name, err)
functionStatistics.FailedToCreate++
} else {
functions = append(functions, functionResponse)
Expand Down Expand Up @@ -222,7 +223,7 @@ func (client *appSyncClient) CreateOrUpdateResolvers(apiID string, resolversFile
}
_, err := client.updateResolver(params)
if err != nil {
logger.Println(fmt.Sprintf("Resolver on type %s and field %s failed to update: %s", resolver.TypeName, resolver.FieldName, err))
logger.Printf("Resolver on type %s and field %s failed to update: %s\n", resolver.TypeName, resolver.FieldName, err)
resolverStatistics.FailedToUpdate++
} else {
resolverStatistics.Updated++
Expand All @@ -240,7 +241,7 @@ func (client *appSyncClient) CreateOrUpdateResolvers(apiID string, resolversFile
}
_, err := client.createResolver(params)
if err != nil {
logger.Println(fmt.Sprintf("Resolver on type %s and field %s failed to create: %s", resolver.TypeName, resolver.FieldName, err))
logger.Printf("Resolver on type %s and field %s failed to create: %s\n", resolver.TypeName, resolver.FieldName, err)
resolverStatistics.FailedToCreate++
} else {
resolverStatistics.Created++
Expand Down Expand Up @@ -396,3 +397,28 @@ func (client *appSyncClient) GetSchemaCreationStatus(apiID string) (string, stri
creationStatus, creationDetails, err := client.getSchemaCreationStatus(schemaStatusParams)
return creationStatus, creationDetails, err
}

func (client *appSyncClient) StartPartialSchemaCreationOrUpdate(apiID string, schema []byte, resolverFile []byte) error {
out, err := client.appSyncClient.GetIntrospectionSchema(&appsync.GetIntrospectionSchemaInput{
ApiId: aws.String(apiID),
Format: aws.String("SDL"),
IncludeDirectives: aws.Bool(true),
})

if err != nil {
return err
}

var resolvers Resolvers
err = yaml.Unmarshal(resolverFile, &resolvers)
if err != nil {
return err
}

newSchema, err := combineSchemas(string(out.Schema), string(schema), resolvers.Resolvers)
if err != nil {
return err
}

return client.StartSchemaCreationOrUpdate(apiID, []byte(newSchema))
}
2 changes: 2 additions & 0 deletions cmd/out/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ func main() {
apiID := os.Getenv("INPUT_API_ID")
schemaFile := os.Getenv("INPUT_SCHEMA_FILE")
resolversFile := os.Getenv("INPUT_RESOLVERS_FILE")
partialUpdate := os.Getenv("PARTIAL_UPDATE")

input.Source = map[string]string{
"api_id": apiID,
Expand All @@ -43,6 +44,7 @@ func main() {
input.Params = map[string]string{
"schema_file": schemaFile,
"resolvers_file": resolversFile,
"partial_update": partialUpdate,
}

} else if err := decoder.Decode(&input); err != nil {
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,7 @@ go 1.13

require (
github.com/aws/aws-sdk-go v1.38.25
github.com/graphql-go/graphql v0.8.0 // indirect
github.com/stretchr/testify v1.8.1 // indirect
gopkg.in/yaml.v2 v2.2.8
)
14 changes: 14 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,25 @@ github.com/aws/aws-sdk-go v1.15.68/go.mod h1:E3/ieXAlvM0XWO57iftYVDLLvQ824smPP3A
github.com/aws/aws-sdk-go v1.38.25 h1:aNjeh7+MON05cZPtZ6do+KxVT67jPOSQXANA46gOQao=
github.com/aws/aws-sdk-go v1.38.25/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/graphql-go/graphql v0.8.0 h1:JHRQMeQjofwqVvGwYnr8JnPTY0AxgVy1HpHSGPLdH0I=
github.com/graphql-go/graphql v0.8.0/go.mod h1:nKiHzRM0qopJEwCITUuIsxk9PlVlwIiiI8pnJEhordQ=
github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8 h1:12VvqtR6Aowv3l/EQUlocDHW2Cp4G9WJVH7uyH8QFJE=
github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
Expand All @@ -24,3 +35,6 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
204 changes: 204 additions & 0 deletions graphqlUtils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
package resource

import (
"github.com/graphql-go/graphql/language/ast"
"github.com/graphql-go/graphql/language/parser"
"github.com/graphql-go/graphql/language/printer"
)

func combineSchemas(oldSDL string, newSDL string, resolvers []Resolver) (string, error) {
oldSchema, err := parser.Parse(parser.ParseParams{
Source: string(oldSDL),
Options: parser.ParseOptions{
NoLocation: false,
NoSource: true,
},
})

if err != nil {
return "", err
}

newSchema, err := parser.Parse(parser.ParseParams{
Source: string(newSDL),
Options: parser.ParseOptions{
NoLocation: false,
NoSource: true,
},
})

if err != nil {
return "", err
}

// Ensure that resolved fields are in the schema
for _, resolver := range resolvers {
existingType := getTypeFromSchema(oldSchema, resolver.TypeName)

if existingType == nil {
continue
}

existingObjectType := existingType.(*ast.ObjectDefinition)

newType := getTypeFromSchema(newSchema, resolver.TypeName).(*ast.ObjectDefinition)
newField := getFieldFromObjectDefinition(newType, resolver.FieldName)

insertTypeAndSubtypesIntoSchema(oldSchema, newSchema, newField.Type)
for _, arg := range newField.Arguments {
insertTypeAndSubtypesIntoSchema(oldSchema, newSchema, arg.Type)
}

replaceFieldInObjectDefinition(existingObjectType, newField)
}

printed := printer.Print(oldSchema)

return printed.(string), nil
}

func getTypeFromSchema(doc *ast.Document, typeName string) ast.Node {
for _, def := range doc.Definitions {
switch def := def.(type) {
case *ast.ObjectDefinition:
if def.Name.Value == typeName {
return def
}
case *ast.InterfaceDefinition:
if def.Name.Value == typeName {
return def
}
case *ast.UnionDefinition:
if def.Name.Value == typeName {
return def
}
case *ast.ScalarDefinition:
if def.Name.Value == typeName {
return def
}
case *ast.EnumDefinition:
if def.Name.Value == typeName {
return def
}
case *ast.InputObjectDefinition:
if def.Name.Value == typeName {
return def
}
}
}
return nil
}

func replaceOrInsertTypeInSchema(doc *ast.Document, typeName string, typ ast.Node) {
for i, def := range doc.Definitions {
switch def := def.(type) {
case *ast.ObjectDefinition:
if def.Name.Value == typeName {
doc.Definitions[i] = typ
return
}
case *ast.InterfaceDefinition:
if def.Name.Value == typeName {
doc.Definitions[i] = typ
return
}
case *ast.UnionDefinition:
if def.Name.Value == typeName {
doc.Definitions[i] = typ
return
}
case *ast.ScalarDefinition:
if def.Name.Value == typeName {
doc.Definitions[i] = typ
return
}
case *ast.EnumDefinition:
if def.Name.Value == typeName {
doc.Definitions[i] = typ
return
}
case *ast.InputObjectDefinition:
if def.Name.Value == typeName {
doc.Definitions[i] = typ
return
}
}
}

doc.Definitions = append(doc.Definitions, typ)
}

func getFieldFromObjectDefinition(obj *ast.ObjectDefinition, fieldName string) *ast.FieldDefinition {
for _, field := range obj.Fields {
if field.Name.Value == fieldName {
return field
}
}
return nil
}

func replaceFieldInObjectDefinition(obj *ast.ObjectDefinition, field *ast.FieldDefinition) {

for i, existingField := range obj.Fields {
if existingField.Name.Value == field.Name.Value {
obj.Fields[i] = field
return
}
}

obj.Fields = append(obj.Fields, field)
}

func insertTypeAndSubtypesIntoSchema(target *ast.Document, source *ast.Document, typ ast.Type) {
switch typ := typ.(type) {
case *ast.Named:
typeDefinition := getTypeFromSchema(source, typ.Name.Value)

replaceOrInsertTypeInSchema(target, typ.Name.Value, typeDefinition)
insertChildTypes(target, source, typeDefinition)

case *ast.List:
insertTypeAndSubtypesIntoSchema(target, source, typ.Type)
case *ast.NonNull:
insertTypeAndSubtypesIntoSchema(target, source, typ.Type)
}
}

func insertChildTypes(target *ast.Document, source *ast.Document, typ ast.Node) {
switch def := typ.(type) {
case *ast.ObjectDefinition:
for _, field := range def.Fields {
insertTypeAndSubtypesIntoSchema(target, source, field.Type)
for _, arg := range field.Arguments {
insertTypeAndSubtypesIntoSchema(target, source, arg.Type)
}
}
case *ast.InterfaceDefinition:
for _, field := range def.Fields {
insertTypeAndSubtypesIntoSchema(target, source, field.Type)
for _, arg := range field.Arguments {
insertTypeAndSubtypesIntoSchema(target, source, arg.Type)
}
}

for _, obj := range source.Definitions {
if obj, ok := obj.(*ast.ObjectDefinition); ok {
for _, impl := range obj.Interfaces {
if impl.Name.Value == def.Name.Value {
replaceOrInsertTypeInSchema(target, obj.Name.Value, obj)
insertChildTypes(target, source, obj)
}
}
}
}

case *ast.UnionDefinition:
for _, typ := range def.Types {
insertTypeAndSubtypesIntoSchema(target, source, typ)
}
case *ast.InputObjectDefinition:
for _, field := range def.Fields {
insertTypeAndSubtypesIntoSchema(target, source, field.Type)
}
}
}
Loading