소스 검색

Resolver

- refactoring
- schema: add `@goTag`s
- implement `mutation`.`CustomerUpdate`
Alexey Kim 2 년 전
부모
커밋
6d8f247f9f

+ 39 - 0
graphql/mutationCustomerAccessTokenCreate.go

@@ -0,0 +1,39 @@
+package graphql
+
+import (
+	"context"
+	"github.com/Nerzal/gocloak/v11"
+	"github.com/gshopify/service-wrapper/scalar"
+	"gshopper.com/gshopify/customer/graphql/generated"
+)
+
+func (r *mutationResolver) CustomerAccessTokenCreate(
+	ctx context.Context,
+	input generated.CustomerAccessTokenCreateInput) (*generated.CustomerAccessTokenCreatePayload, error) {
+
+	var (
+		token    *gocloak.JWT
+		err      error
+		response = &generated.CustomerAccessTokenCreatePayload{}
+	)
+
+	if token, err = r.client.Login(
+		ctx, r.conf.Cli.ClientId, r.conf.Cli.ClientSecret, r.conf.Cli.Realm, input.Email, input.Password); err != nil {
+		response.CustomerUserErrors = append(response.CustomerUserErrors,
+			CustomerError(generated.CustomerErrorCodeUnidentifiedCustomer, err))
+		return response, nil
+	}
+
+	if err = r.saveSession(ctx, token); err != nil {
+		response.CustomerUserErrors = append(response.CustomerUserErrors,
+			CustomerError(generated.CustomerErrorCodeBlank, err))
+		return response, nil
+	}
+
+	response.CustomerAccessToken = &generated.CustomerAccessToken{
+		AccessToken: token.AccessToken,
+		ExpiresAt:   scalar.NewDateTimeIn(token.RefreshExpiresIn),
+	}
+
+	return response, nil
+}

+ 39 - 0
graphql/mutationCustomerAccessTokenDelete.go

@@ -0,0 +1,39 @@
+package graphql
+
+import (
+	"context"
+	"github.com/gshopify/service-wrapper/auth"
+	"gshopper.com/gshopify/customer/graphql/generated"
+)
+
+func (r *mutationResolver) CustomerAccessTokenDelete(
+	ctx context.Context, t string) (*generated.CustomerAccessTokenDeletePayload, error) {
+
+	var (
+		response  = &generated.CustomerAccessTokenDeletePayload{}
+		session   = auth.SessionManager()
+		sessionId string
+		err       error
+	)
+
+	_, sessionId, err = r.decodeAccessToken(ctx, t)
+	if err != nil {
+		response.UserErrors = append(response.UserErrors, ErrToken(err.Error()))
+		return response, nil
+	}
+
+	refresh, err := session.Token(ctx, sessionId)
+	if err != nil {
+		response.UserErrors = append(response.UserErrors, ErrToken(err.Error()))
+		return response, nil
+	}
+
+	if err = r.client.Logout(ctx, r.conf.Cli.ClientId, r.conf.Cli.ClientSecret, r.conf.Cli.Realm, refresh); err != nil {
+		response.UserErrors = append(response.UserErrors, ErrToken(err.Error()))
+		return response, nil
+	}
+
+	response.DeletedAccessToken = &t
+	response.DeletedCustomerAccessTokenID = &sessionId
+	return response, nil
+}

+ 46 - 0
graphql/mutationCustomerAccessTokenRenew.go

@@ -0,0 +1,46 @@
+package graphql
+
+import (
+	"context"
+	"github.com/gshopify/service-wrapper/auth"
+	"github.com/gshopify/service-wrapper/scalar"
+	"gshopper.com/gshopify/customer/graphql/generated"
+)
+
+func (r *mutationResolver) CustomerAccessTokenRenew(ctx context.Context, t string) (*generated.CustomerAccessTokenRenewPayload, error) {
+	var (
+		response  = &generated.CustomerAccessTokenRenewPayload{}
+		session   = auth.SessionManager()
+		sessionId string
+		err       error
+	)
+
+	_, sessionId, err = r.decodeAccessToken(ctx, t)
+	if err != nil {
+		response.UserErrors = append(response.UserErrors, ErrTokenNotExists)
+		return response, nil
+	}
+
+	refresh, err := session.Token(ctx, sessionId)
+	if err != nil {
+		response.UserErrors = append(response.UserErrors, ErrToken(err.Error()))
+		return response, nil
+	}
+
+	token, err := r.client.RefreshToken(ctx, refresh, r.conf.Cli.ClientId, r.conf.Cli.ClientSecret, r.conf.Cli.Realm)
+	if err != nil {
+		response.UserErrors = append(response.UserErrors, ErrTokenExpired)
+		return response, nil
+	}
+
+	if err = r.saveSession(ctx, token); err != nil {
+		response.UserErrors = append(response.UserErrors, ErrToken(err.Error()))
+		return response, nil
+	}
+
+	response.CustomerAccessToken = &generated.CustomerAccessToken{
+		AccessToken: token.AccessToken,
+		ExpiresAt:   scalar.NewDateTimeIn(token.RefreshExpiresIn),
+	}
+	return response, nil
+}

+ 10 - 0
graphql/mutationCustomerCreate.go

@@ -0,0 +1,10 @@
+package graphql
+
+import (
+	"context"
+	"gshopper.com/gshopify/customer/graphql/generated"
+)
+
+func (r *mutationResolver) CustomerCreate(ctx context.Context, input generated.CustomerCreateInput) (*generated.CustomerCreatePayload, error) {
+	panic("not implemented")
+}

+ 129 - 0
graphql/mutationCustomerUpdate.go

@@ -0,0 +1,129 @@
+package graphql
+
+import (
+	"context"
+	"fmt"
+	"github.com/Nerzal/gocloak/v11"
+	"github.com/gshopify/service-wrapper/server/middleware"
+	"gshopper.com/gshopify/customer/graphql/generated"
+	m "gshopper.com/gshopify/customer/model"
+	"time"
+)
+
+func (r *mutationResolver) CustomerUpdate(
+	ctx context.Context, t string, udata generated.CustomerUpdateInput) (*generated.CustomerUpdatePayload, error) {
+
+	var (
+		inContext, err = middleware.InContext(ctx)
+		response       = &generated.CustomerUpdatePayload{}
+		customer       *generated.Customer
+		phone          *m.Phone
+	)
+
+	if err != nil {
+		return nil, err
+	}
+
+	if _, _, err = r.decodeAccessToken(ctx, t); err != nil {
+		response.CustomerUserErrors = append(response.CustomerUserErrors,
+			CustomerError(generated.CustomerErrorCodeTokenInvalid, err, "customerAccessToken"))
+		return response, nil
+	}
+
+	customer, err = r.customer(ctx, t)
+	if err != nil {
+		response.CustomerUserErrors = append(response.CustomerUserErrors,
+			CustomerError(generated.CustomerErrorCodeTokenInvalid, err, "customerAccessToken"))
+		return response, nil
+	}
+
+	// upsert FirstName
+	if udata.FirstName != nil {
+		customer.FirstName = udata.FirstName
+	}
+
+	// upsert LastName
+	if udata.LastName != nil {
+		customer.LastName = udata.LastName
+	}
+
+	// upsert AcceptsMarketing
+	if udata.AcceptsMarketing != nil {
+		customer.AcceptsMarketing = *udata.AcceptsMarketing
+	}
+
+	if udata.Phone != nil {
+		phone, err = m.NewPhoneNumber(*udata.Phone, inContext.Country.String(), false)
+		if err != nil {
+			response.CustomerUserErrors = append(response.CustomerUserErrors,
+				CustomerError(generated.CustomerErrorCodeInvalid, err, "phone"))
+			return response, nil
+		}
+
+		customer.Phone = gocloak.StringP(phone.String())
+	}
+
+	user := gocloak.User{
+		ID:        gocloak.StringP(customer.ID),
+		FirstName: customer.FirstName,
+		LastName:  customer.LastName,
+		Email:     customer.Email,
+		Attributes: &map[string][]string{
+			"accepts_marketing": {fmt.Sprintf("%v", customer.AcceptsMarketing)},
+			"phone_number":      {*customer.Phone},
+		},
+	}
+
+	customer.Metafields = append(customer.Metafields, &generated.Metafield{
+		Namespace: "customer",
+		Key:       "updated_at",
+		Type:      "date_time",
+		Value:     fmt.Sprintf("%d", time.Now().UnixMilli()),
+	})
+
+	for _, metafield := range customer.Metafields {
+		(*user.Attributes)[metafield.Key] = []string{metafield.Value}
+	}
+
+	// update user properties & attributes
+	admin, err := r.admin(ctx)
+	if err != nil {
+		response.CustomerUserErrors = append(response.CustomerUserErrors,
+			CustomerError(generated.CustomerErrorCodeTokenInvalid, err, "adminAccessToken"))
+		return response, nil
+	}
+
+	if err := r.client.UpdateUser(ctx, admin.AccessToken, r.conf.Cli.Realm, user); err != nil {
+		response.CustomerUserErrors = append(response.CustomerUserErrors,
+			CustomerError(generated.CustomerErrorCodeInvalid, err))
+		return response, nil
+	}
+
+	// update user password
+	if udata.Password != nil {
+		err = r.client.SetPassword(ctx, admin.AccessToken, customer.ID, r.conf.Cli.Realm, *udata.Password, false)
+		if err != nil {
+			response.CustomerUserErrors = append(response.CustomerUserErrors,
+				CustomerError(generated.CustomerErrorCodeInvalid, err, "password"))
+			return response, nil
+		}
+
+		_, _ = r.CustomerAccessTokenDelete(ctx, t)
+		var payload *generated.CustomerAccessTokenCreatePayload
+		payload, err = r.CustomerAccessTokenCreate(ctx, generated.CustomerAccessTokenCreateInput{
+			Email:    *customer.Email,
+			Password: *udata.Password,
+		})
+
+		if err != nil {
+			response.CustomerUserErrors = append(response.CustomerUserErrors,
+				CustomerError(generated.CustomerErrorCodeInvalid, err, "password"))
+			return response, nil
+		}
+
+		response.CustomerAccessToken = payload.CustomerAccessToken
+	}
+
+	response.Customer = customer
+	return response, nil
+}

+ 14 - 0
graphql/queryCustomer.go

@@ -0,0 +1,14 @@
+package graphql
+
+import (
+	"context"
+	"gshopper.com/gshopify/customer/graphql/generated"
+)
+
+func (r *queryResolver) Customer(ctx context.Context, t string) (*generated.Customer, error) {
+	if _, _, err := r.decodeAccessToken(ctx, t); err != nil {
+		return nil, err
+	}
+
+	return r.customer(ctx, t)
+}

+ 63 - 102
graphql/resolver_impl.go

@@ -7,16 +7,20 @@ import (
 	"github.com/golang-jwt/jwt/v4"
 	"github.com/gshopify/service-wrapper/auth"
 	"github.com/gshopify/service-wrapper/config"
-	"github.com/gshopify/service-wrapper/model"
-	"github.com/gshopify/service-wrapper/scalar"
+	"github.com/mitchellh/mapstructure"
 	"gshopper.com/gshopify/customer/graphql/generated"
-	"strings"
+	m "gshopper.com/gshopify/customer/model"
+	"sync"
 	"time"
 )
 
 type Resolver struct {
 	conf   *auth.Config
 	client gocloak.GoCloak
+
+	mu         sync.Mutex
+	_token     *gocloak.JWT
+	_expiresAt time.Time
 }
 
 func NewResolver() (*Resolver, error) {
@@ -33,12 +37,11 @@ func NewResolver() (*Resolver, error) {
 }
 
 func (r *Resolver) decodeAccessToken(ctx context.Context, t string) (*jwt.Token, string, error) {
-	t = strings.TrimSpace(t)
 	if t == "" {
 		return nil, "", fmt.Errorf("could not decode accessToken: Token is empty")
 	}
 
-	token, claim, err := r.client.DecodeAccessToken(ctx, t, r.conf.Realm)
+	token, claim, err := r.client.DecodeAccessToken(ctx, t, r.conf.Cli.Realm)
 	if err != nil {
 		return nil, "", err
 	}
@@ -61,125 +64,83 @@ func (r *Resolver) decodeAccessToken(ctx context.Context, t string) (*jwt.Token,
 	return token, sessionId, nil
 }
 
-func (r *mutationResolver) CustomerAccessTokenCreate(
-	ctx context.Context,
-	input generated.CustomerAccessTokenCreateInput) (*generated.CustomerAccessTokenCreatePayload, error) {
-
+func (r *Resolver) customer(ctx context.Context, t string) (*generated.Customer, error) {
 	var (
-		token *gocloak.JWT
-		err   error
-
-		session  = auth.SessionManager()
-		response = &generated.CustomerAccessTokenCreatePayload{}
+		customer = generated.Customer{}
+		udata    map[string]any
+		phone    *m.Phone
+		err      error
 	)
 
-	if token, err = r.client.Login(ctx, r.conf.ClientId, r.conf.ClientSecret, r.conf.Realm, input.Email, input.Password); err != nil {
-		response.CustomerUserErrors = append(response.CustomerUserErrors,
-			CustomerError(generated.CustomerErrorCodeUnidentifiedCustomer, err))
-		return response, nil
-	}
-
-	if err = session.PutToken(ctx, token.SessionState, token.RefreshToken, time.Duration(token.RefreshExpiresIn)*time.Second); err != nil {
-		response.CustomerUserErrors = append(response.CustomerUserErrors,
-			CustomerError(generated.CustomerErrorCodeTokenInvalid, err))
-		return response, nil
-	}
-
-	response.CustomerAccessToken = &generated.CustomerAccessToken{
-		AccessToken: token.AccessToken,
-		ExpiresAt:   scalar.NewDateTimeIn(token.RefreshExpiresIn).String(),
-	}
-
-	return response, nil
-}
-
-func (r *mutationResolver) CustomerAccessTokenRenew(ctx context.Context, t string) (*generated.CustomerAccessTokenRenewPayload, error) {
-	var (
-		session  = auth.SessionManager()
-		response = &generated.CustomerAccessTokenRenewPayload{}
-	)
-
-	_, sid, err := r.decodeAccessToken(ctx, t)
+	udata, err = r.client.GetRawUserInfo(ctx, t, r.conf.Cli.Realm)
 	if err != nil {
-		response.UserErrors = append(response.UserErrors, ErrTokenNotExists)
-		return response, nil
+		return nil, err
 	}
 
-	refresh, err := session.Token(ctx, sid)
-	if err != nil {
-		response.UserErrors = append(response.UserErrors, ErrToken(err.Error()))
-		return response, nil
+	if err = mapstructure.Decode(udata, &customer); err != nil {
+		return nil, err
 	}
 
-	token, err := r.client.RefreshToken(ctx, refresh, r.conf.ClientId, r.conf.ClientSecret, r.conf.Realm)
+	customer.Phone = nil
+	phone, err = m.ParsePhoneNumber(udata)
 	if err != nil {
-		response.UserErrors = append(response.UserErrors, ErrTokenExpired)
-		return response, nil
-	}
-
-	if err = session.PutToken(ctx, token.SessionState, token.RefreshToken, time.Duration(token.RefreshExpiresIn)*time.Second); err != nil {
-		response.UserErrors = append(response.UserErrors, ErrToken(err.Error()))
-		return response, nil
+		return nil, err
 	}
 
-	response.CustomerAccessToken = &generated.CustomerAccessToken{
-		AccessToken: token.AccessToken,
-		ExpiresAt:   scalar.NewDateTimeIn(token.RefreshExpiresIn).String(),
-	}
-	return response, nil
+	customer.Phone = gocloak.StringP(phone.String())
+	customer.Metafields = append(customer.Metafields,
+		&generated.Metafield{
+			Namespace: "customer",
+			Key:       "phone_region",
+			Type:      "single_line_text_field",
+			Value:     phone.PhoneRegion,
+		},
+		&generated.Metafield{
+			Namespace: "customer",
+			Key:       "phone_verified",
+			Type:      "boolean",
+			Value:     fmt.Sprintf("%v", phone.Verified),
+		})
+
+	return &customer, nil
 }
 
-func (r *mutationResolver) CustomerAccessTokenDelete(ctx context.Context, t string) (*generated.CustomerAccessTokenDeletePayload, error) {
+func (r *Resolver) admin(ctx context.Context) (*gocloak.JWT, error) {
 	var (
-		response = &generated.CustomerAccessTokenDeletePayload{}
-		session  = auth.SessionManager()
+		creds = r.conf.Admin
+		err   error
 	)
 
-	_, sid, err := r.decodeAccessToken(ctx, t)
-	if err != nil {
-		response.UserErrors = append(response.UserErrors, ErrToken(err.Error()))
-		return response, nil
-	}
-
-	refresh, err := session.Token(ctx, sid)
-	if err != nil {
-		response.UserErrors = append(response.UserErrors, ErrToken(err.Error()))
-		return response, nil
-	}
+	if time.Now().After(r._expiresAt) {
+		r.mu.Lock()
+		defer r.mu.Unlock()
 
-	if err = r.client.Logout(ctx, r.conf.ClientId, r.conf.ClientSecret, r.conf.Realm, refresh); err != nil {
-		response.UserErrors = append(response.UserErrors, ErrToken(err.Error()))
-		return response, nil
-	}
+		if r._token != nil {
+			r._token, err = r.client.RefreshToken(ctx, r._token.RefreshToken, creds.ClientId, creds.ClientSecret, creds.Realm)
+		}
 
-	response.DeletedAccessToken = &t
-	response.DeletedCustomerAccessTokenID = &sid
-	return response, nil
-}
+		if r._token == nil || err != nil {
+			r._token, err = r.client.LoginAdmin(ctx, creds.Login, creds.Password, creds.Realm)
+		}
 
-func (r *queryResolver) Customer(ctx context.Context, t string) (*generated.Customer, error) {
-	if _, _, err := r.decodeAccessToken(ctx, t); err != nil {
-		return nil, err
+		if r._token == nil || err != nil {
+			r._expiresAt = time.Time{}
+		} else {
+			r._expiresAt = time.Now().
+				Add(time.Duration(r._token.ExpiresIn) * time.Second).
+				Add(-time.Second * 5)
+		}
 	}
 
-	userinfo, err := r.client.GetUserInfo(ctx, t, r.conf.Realm)
-	if err != nil {
-		return nil, err
-	}
+	return r._token, err
+}
 
-	return &generated.Customer{
-		AcceptsMarketing: false, //TODO:
-		Addresses:        nil,   //TODO:
-		DefaultAddress:   nil,   //TODO:
-		DisplayName:      *userinfo.PreferredUsername,
-		Email:            userinfo.Email,
-		FirstName:        userinfo.Name,
-		ID:               *userinfo.Sub,
-		LastName:         userinfo.FamilyName,
-		NumberOfOrders:   model.UInt(0), //TODO:
-		Phone:            userinfo.PhoneNumber,
-		Tags:             nil, //TODO:
-	}, nil
+func (r *Resolver) saveSession(ctx context.Context, token *gocloak.JWT) error {
+	return auth.SessionManager().PutToken(
+		ctx,
+		token.SessionState,
+		token.RefreshToken,
+		time.Duration(token.RefreshExpiresIn)*time.Second)
 }
 
 // Mutation returns generated.MutationResolver implementation.

+ 95 - 16
graphql/schema.graphql

@@ -1,3 +1,8 @@
+directive @goTag(
+  key: String!
+  value: String
+) on INPUT_FIELD_DEFINITION | FIELD_DEFINITION
+
 scalar DateTime
 scalar UnsignedInt64
 scalar URL
@@ -24,6 +29,12 @@ type Mutation {
 
   # Permanently destroys a customer access token.
   customerAccessTokenDelete(customerAccessToken: String!): CustomerAccessTokenDeletePayload
+
+  # Creates a new customer.
+  customerCreate(input: CustomerCreateInput!): CustomerCreatePayload
+
+  # Updates an existing customer.
+  customerUpdate(customerAccessToken: String! customer: CustomerUpdateInput!): CustomerUpdatePayload
 }
 
 type Page implements Node&HasMetafields&OnlineStorePublishable{
@@ -185,7 +196,7 @@ type PageInfo {
 # saving logged-in customers the trouble of having to provide it at every checkout.
 type Customer implements HasMetafields {
   # Indicates whether the customer has consented to be sent marketing material via email.
-  acceptsMarketing: Boolean!
+  acceptsMarketing: Boolean! @goTag(key: "mapstructure" value: "accepts_marketing")
 
   # A list of addresses for the customer.
   addresses(
@@ -194,40 +205,40 @@ type Customer implements HasMetafields {
     first: Int
     last: Int
     reverse: Boolean = false
-  ): MailingAddressConnection!
+  ): MailingAddressConnection! @goTag(key: "mapstructure" value: "addresses")
 
   # The date and time when the customer was created.
-  createdAt: DateTime!
+  createdAt: DateTime! @goTag(key: "mapstructure" value: "created_at")
 
   # The customer’s default address.
-  defaultAddress: MailingAddress
+  defaultAddress: MailingAddress @goTag(key: "mapstructure" value: "default_address")
 
   # The customer’s name, email or phone number.
-  displayName: String!
+  displayName: String! @goTag(key: "mapstructure" value: "name")
 
   # The customer’s email address.
-  email: String
+  email: String @goTag(key: "mapstructure" value: "email")
 
   # The customer’s first name.
-  firstName: String
+  firstName: String @goTag(key: "mapstructure" value: "given_name")
 
   # A unique identifier for the customer.
-  id: ID!
+  id: ID! @goTag(key: "mapstructure" value: "sub")
 
   # The customer's most recently updated, incomplete checkout.
   # lastIncompleteCheckout: Checkout
 
   # The customer’s last name.
-  lastName: String
+  lastName: String @goTag(key: "mapstructure" value: "family_name")
 
   # Returns a metafield found by namespace and key.
-  metafield(key: String!namespace: String!): Metafield
+  metafield(key: String!namespace: String!): Metafield @goTag(key: "mapstructure" value: "metafield")
 
   # The metafields associated with the resource matching the supplied list of namespaces and keys.
-  metafields(identifiers: [HasMetafieldsIdentifier!]!): [Metafield]!
+  metafields(identifiers: [HasMetafieldsIdentifier!]!): [Metafield]! @goTag(key: "mapstructure" value: "metafields")
 
   # The number of orders that the customer has made at the store in their lifetime.
-  numberOfOrders: UnsignedInt64!
+  numberOfOrders: UnsignedInt64! @goTag(key: "mapstructure" value: "number_of_orders")
 
   # The orders associated with the customer.
 #  orders(
@@ -238,17 +249,17 @@ type Customer implements HasMetafields {
 #    query: String
 #    reverse: Boolean = false
 #    sortKey: OrderSortKeys = ID
-#  ): OrderConnection!
+#  ): OrderConnection! @goTag(key: "mapstructure" value: "orders")
 
   # The customer’s phone number.
-  phone: String
+  phone: String @goTag(key: "mapstructure" value: "phone_number")
 
   # A comma separated list of tags that have been added to the customer.
   # Additional access scope required: unauthenticated_read_customer_tags.
-  tags: [String!]!
+  tags: [String!]! @goTag(key: "mapstructure" value: "tags")
 
   # The date and time when the customer information was updated.
-  updatedAt: DateTime!
+  updatedAt: DateTime! @goTag(key: "mapstructure" value: "updated_at")
 }
 
 # An auto-generated type for paginating through multiple MailingAddresses.
@@ -433,6 +444,74 @@ type CustomerAccessTokenDeletePayload {
   userErrors: [UserError!]!
 }
 
+# Specifies the fields required to update the Customer information.
+input CustomerUpdateInput {
+  # The customer’s first name.
+  firstName: String
+
+  # The customer’s last name.
+  lastName: String
+
+  # The customer’s email.
+  email: String
+
+  # A unique phone number for the customer.
+  #
+  # Formatted using E.164 standard. For example, +16135551111. To remove the phone number, specify null.
+  phone: String
+
+  # The login password used by the customer.
+  password: String
+
+  # Indicates whether the customer has consented to be sent marketing material via email.
+  acceptsMarketing: Boolean
+}
+
+# The fields required to create a new customer.
+input CustomerCreateInput {
+  # The customer’s first name.
+  firstName: String
+
+  # The customer’s last name.
+  lastName: String
+
+  # The customer’s email.
+  email: String!
+
+  # A unique phone number for the customer.
+  #
+  # Formatted using E.164 standard. For example, +16135551111.
+  phone: String
+
+  # The login password used by the customer.
+  password: String!
+
+  # Indicates whether the customer has consented to be sent marketing material via email.
+  acceptsMarketing: Boolean
+}
+
+# Return type for `customerCreate` mutation.
+type CustomerCreatePayload {
+  # The created customer object.
+  customer: Customer
+
+  # The list of errors that occurred from executing the mutation.
+  customerUserErrors: [CustomerUserError!]!
+}
+
+# Return type for `customerUpdate` mutation.
+type CustomerUpdatePayload {
+  # The updated customer object.
+  customer: Customer
+
+  # The newly created customer access token. If the customer's password is updated, all previous access tokens
+  # (including the one used to perform this mutation) become invalid, and a new token is generated.
+  customerAccessToken: CustomerAccessToken
+
+  # The list of errors that occurred from executing the mutation.
+  customerUserErrors: [CustomerUserError!]!
+}
+
 # ISO 639-1 language codes supported by Shopify.
 enum LanguageCode {
   # Afrikaans