2
0
Pārlūkot izejas kodu

ProductVariant

- impl `title`
- impl `selectedOptions`
Alexey Kim 2 gadi atpakaļ
vecāks
revīzija
515ab1a8b7

+ 36 - 30
db/clickhouse.go

@@ -188,39 +188,13 @@ func (db *clickhouse) ProductOptions(ln model.LanguageCode, id string) ([]*gener
 
 func (db *clickhouse) ProductVariants(ctx *middleware.GShopifyContext, id string) ([]*generated.ProductVariant, error) {
 	var (
-		variants  []*generated.ProductVariant
-		selection = []string{
-			"any(t.id) as id",
-			"any(t.product_id) as product_id",
-			"any(t.inventory_item_id) as inventory_item_id",
-			fmt.Sprintf("any(t.price['%s']) as price", defaultCurrency),
-			fmt.Sprintf("any(t.compare_at_price['%s']) as compare_at_price", defaultCurrency),
-			"any(t.position) as position",
-			"any(t.image) as image",
-			"any(t.inventory_management) as inventory_management",
-			"any(t.inventory_policy) as inventory_policy",
-			"any(t.options) as options",
-			"any(t.grams) as grams",
-			"any(t.weight) as weight",
-			"any(t.weight_unit) as weight_unit",
-			"any(t.created_at) as created_at",
-			"any(t.updated_at) as updated_at",
-			"any(t.published_at) as published_at",
-			"any(t.deleted_at) as deleted_at",
-			"sum(inventory_level.available) as available",
-			fmt.Sprintf("any(product.title['%s']) as title", ctx.Language),
-			"any(inventory_item.sku) as sku",
-			"any(inventory_item.barcode) as barcode",
-			"any(inventory_item.requires_shipping) as requires_shipping",
-			"any(inventory_item.tracked) as tracked",
-			"if(tracked, if(available > 0, true, if(inventory_policy == 'continue', true, false)), true) as for_sale",
-		}
-		key = productVariantKey("t.product_id=?", id)
-		l   = ttlcache.LoaderFunc[string, any](
+		variants []*generated.ProductVariant
+		key      = productVariantKey("t.product_id=?", id)
+		l        = ttlcache.LoaderFunc[string, any](
 			func(ttl *ttlcache.Cache[string, any], _ string) *ttlcache.Item[string, any] {
 				var o []relation.ProductVariant
 				rows, err := db.session.
-					Select(selection...).
+					Select(productVariantSelection(ctx.Language, defaultCurrency)...).
 					From(fmt.Sprintf("%s as t", key.Table())).
 					LeftJoin("product", "product.id = t.product_id").
 					LeftJoin("inventory_item", "inventory_item.id = t.inventory_item_id").
@@ -285,6 +259,38 @@ func (db *clickhouse) CollectionProducts(ln model.LanguageCode, id string) ([]*g
 	return products, nil
 }
 
+func (db *clickhouse) ProductVariantOptions(ln model.LanguageCode, id string) ([]*generated.SelectedOption, error) {
+	var (
+		options []*generated.SelectedOption
+		key     = productOptionKey("product_variant.id = ?", id)
+		l       = ttlcache.LoaderFunc[string, any](
+			func(ttl *ttlcache.Cache[string, any], _ string) *ttlcache.Item[string, any] {
+				var o []relation.ProductOption
+				rows, err := db.session.SelectBySql("SELECT "+
+					strings.Join(productOptionSelectedSelection(ln), ", ")+
+					" FROM `"+key.Table()+"`"+
+					" ARRAY JOIN (SELECT `options` from `product_variant` where `id` = ?) as opt"+
+					" WHERE id = tupleElement(opt, 1)"+
+					" ORDER BY position", key.Args()).
+					Load(&o)
+				if rows < 1 || err != nil {
+					return nil
+				}
+
+				return ttl.Set(key.String(), o, key.TTL())
+			},
+		)
+	)
+
+	if p := db.cache.Get(key.String(), ttlcache.WithLoader[string, any](l)); p != nil {
+		for _, option := range p.Value().([]relation.ProductOption) {
+			options = append(options, option.AsSelected())
+		}
+	}
+
+	return options, nil
+}
+
 func (db *clickhouse) Ping() error {
 	return db.session.Ping()
 }

+ 1 - 0
db/database.go

@@ -20,5 +20,6 @@ type Database interface {
 	ProductCollections(ln model.LanguageCode, id string) ([]*generated.Collection, error)
 	ProductOptions(ln model.LanguageCode, id string) ([]*generated.ProductOption, error)
 	ProductVariants(ctx *middleware.GShopifyContext, id string) ([]*generated.ProductVariant, error)
+	ProductVariantOptions(ln model.LanguageCode, id string) ([]*generated.SelectedOption, error)
 	CollectionProducts(ln model.LanguageCode, id string) ([]*generated.Product, error)
 }

+ 43 - 4
db/product.go

@@ -29,14 +29,14 @@ var (
 			"any(inventory_item.tracked) as tracked",
 			"any(inventory_level.available) as available",
 			"if(tracked, if(available > 0, true, if(inventory_policy == 'continue', true, false)), true) as for_sale",
-			fmt.Sprintf("min(product_variant.price['%s']) as price_min", currency),
-			fmt.Sprintf("max(product_variant.price['%s']) as price_max", currency),
-			fmt.Sprintf("min(product_variant.compare_at_price['%s']) as compare_at_price_min", currency),
-			fmt.Sprintf("max(product_variant.compare_at_price['%s']) as compare_at_price_max", currency),
 			"any(created_at) as created_at", "any(updated_at) as updated_at", "any(published_at) as published_at", "any(deleted_at) as deleted_at",
 		},
 			ln.SqlFieldSelection("title", "", "any"),
 			ln.SqlFieldSelection("description", "", "any"),
+			fmt.Sprintf("min(product_variant.price['%s']) as price_min", currency),
+			fmt.Sprintf("max(product_variant.price['%s']) as price_max", currency),
+			fmt.Sprintf("min(product_variant.compare_at_price['%s']) as compare_at_price_min", currency),
+			fmt.Sprintf("max(product_variant.compare_at_price['%s']) as compare_at_price_max", currency),
 		)
 	}
 	productCollectionSelection = func(ln model.LanguageCode) []string {
@@ -62,12 +62,51 @@ var (
 			ln.SqlArraySelection("values"),
 		)
 	}
+	productOptionSelectedSelection = func(ln model.LanguageCode) []string {
+		return append([]string{
+			"id",
+			"if(notEmpty(value_ln), value_ln, value_en) as value",
+			"position", "created_at", "updated_at", "published_at", "deleted_at",
+		},
+			ln.SqlFieldSelection("name", "", ""),
+			fmt.Sprintf("arrayElement(values, tupleElement(opt, 2))['%s'] as value_en", model.LanguageCodeEn),
+			fmt.Sprintf("arrayElement(values, tupleElement(opt, 2))['%s'] as value_ln", ln),
+		)
+	}
 	productOptionKey = func(clause string, args ...any) *cache.SqlKey {
 		return cache.NewSQLKey(
 			"product_option",
 			time.Minute,
 			clause, args...)
 	}
+	productVariantSelection = func(ln model.LanguageCode, currency generated.CurrencyCode) []string {
+		return append([]string{
+			"any(t.id) as id",
+			"any(t.product_id) as product_id",
+			"any(t.inventory_item_id) as inventory_item_id",
+			"any(t.position) as position",
+			"any(t.image) as image",
+			"any(t.inventory_management) as inventory_management",
+			"any(t.inventory_policy) as inventory_policy",
+			"any(t.grams) as grams",
+			"any(t.weight) as weight",
+			"any(t.weight_unit) as weight_unit",
+			"any(t.created_at) as created_at",
+			"any(t.updated_at) as updated_at",
+			"any(t.published_at) as published_at",
+			"any(t.deleted_at) as deleted_at",
+			"sum(inventory_level.available) as available",
+			"any(inventory_item.sku) as sku",
+			"any(inventory_item.barcode) as barcode",
+			"any(inventory_item.requires_shipping) as requires_shipping",
+			"any(inventory_item.tracked) as tracked",
+			"if(tracked, if(available > 0, true, if(inventory_policy == 'continue', true, false)), true) as for_sale",
+		},
+			ln.SqlFieldSelection("title", "product", "any"),
+			fmt.Sprintf("any(t.price['%s']) as price", currency),
+			fmt.Sprintf("any(t.compare_at_price['%s']) as compare_at_price", currency),
+		)
+	}
 	productVariantKey = func(clause string, args ...any) *cache.SqlKey {
 		return cache.NewSQLKey(
 			"product_variant",

+ 35 - 6
graphql/query_product.go

@@ -9,6 +9,7 @@ import (
 	"github.com/microcosm-cc/bluemonday"
 	"gshopper.com/gshopify/products/graphql/generated"
 	"gshopper.com/gshopify/products/graphql/helper"
+	"strings"
 )
 
 func (r *queryResolver) Product(ctx context.Context, handle *string, id *string) (*generated.Product, error) {
@@ -126,7 +127,6 @@ func (r *productResolver) Variants(ctx context.Context, product *generated.Produ
 func (r *productVariantResolver) Product(ctx context.Context, variant *generated.ProductVariant) (*generated.Product, error) {
 	var (
 		inContext *middleware.GShopifyContext
-		product   *generated.Product
 		err       error
 	)
 
@@ -143,14 +143,43 @@ func (r *productVariantResolver) Product(ctx context.Context, variant *generated
 		return nil, err
 	}
 
-	if product, err = r.db.Product(inContext.Language, nil, &rawId); err != nil {
-		return nil, err
-	}
-
-	return product, nil
+	return r.db.Product(inContext.Language, nil, &rawId)
 }
 
+// StoreAvailability TODO: implement
 func (r *productVariantResolver) StoreAvailability(ctx context.Context, variant *generated.ProductVariant,
 	first *int, after *string, last *int, before *string, reverse *bool) (*generated.StoreAvailabilityConnection, error) {
 	panic("not implemented")
 }
+
+func (r *productVariantResolver) SelectedOptions(ctx context.Context, variant *generated.ProductVariant) ([]*generated.SelectedOption, error) {
+	var (
+		inContext *middleware.GShopifyContext
+		err       error
+	)
+
+	if inContext, err = middleware.InContext(ctx); err != nil {
+		return nil, err
+	}
+
+	rawId, err := model.ParseId(model.GidVariant, variant.ID)
+	if err != nil {
+		return nil, fmt.Errorf("unknown ID for variant: `%s`", variant.ID)
+	}
+
+	return r.db.ProductVariantOptions(inContext.Language, rawId)
+}
+
+func (r *productVariantResolver) Title(ctx context.Context, variant *generated.ProductVariant) (string, error) {
+	opts, err := r.SelectedOptions(ctx, variant)
+	if err != nil {
+		return "", err
+	}
+
+	s := make([]string, len(opts))
+	for i := range opts {
+		s[i] = opts[i].Value
+	}
+
+	return strings.Join(s, " / "), nil
+}

+ 2 - 2
graphql/schema.graphql

@@ -618,7 +618,7 @@ type ProductVariant implements Node&HasMetafields {
     requiresShipping: Boolean!
 
     # List of product options applied to the variant.
-    selectedOptions: [SelectedOption!]!
+    selectedOptions: [SelectedOption!]! @goField(forceResolver: true)
 
     # Represents an association between a variant and a selling plan. Selling plan allocations describe which selling plans are available for each variant, and what their impact is on pricing.
     sellingPlanAllocations(
@@ -642,7 +642,7 @@ type ProductVariant implements Node&HasMetafields {
     ): StoreAvailabilityConnection! @goField(forceResolver: true)
 
     # The product variant’s title.
-    title: String!
+    title: String! @goField(forceResolver: true)
 
     # The unit price value for the variant based on the variant's measurement.
     unitPrice: MoneyV2

+ 9 - 12
relation/product.go

@@ -67,18 +67,14 @@ func (p *Product) As() *generated.Product {
 				CurrencyCode: generated.CurrencyCodeUsd,
 			},
 		},
-		TotalInventory: func() *int {
-			i := p.QuantityAvailable
-			return &i
-		}(),
 
 		RequiresSellingPlan:      false, // TODO:
-		FeaturedImage:            nil,
-		Images:                   nil,
-		Media:                    nil,
-		OnlineStoreURL:           nil,
-		SellingPlanGroups:        nil,
-		VariantBySelectedOptions: nil,
+		FeaturedImage:            nil,   // TODO:
+		Images:                   nil,   // TODO:
+		Media:                    nil,   // TODO:
+		OnlineStoreURL:           nil,   // TODO:
+		SellingPlanGroups:        nil,   // TODO:
+		VariantBySelectedOptions: nil,   // TODO:
 
 		ID:              model.NewId(model.GidProduct, p.Id),
 		Handle:          p.Handle.String,
@@ -93,8 +89,9 @@ func (p *Product) As() *generated.Product {
 			Description: &description,
 			Title:       &p.Title,
 		},
-		CreatedAt: scalar.NewDateTimeFrom(p.CreatedAt),
-		UpdatedAt: scalar.NewDateTimeFrom(p.UpdatedAt),
+		TotalInventory: &p.QuantityAvailable,
+		CreatedAt:      scalar.NewDateTimeFrom(p.CreatedAt),
+		UpdatedAt:      scalar.NewDateTimeFrom(p.UpdatedAt),
 	}
 
 	if p.PublishedAt != nil {

+ 16 - 8
relation/product_option.go

@@ -7,14 +7,15 @@ import (
 )
 
 type ProductOption struct {
-	Id          string     `db:"id"`
-	Name        string     `db:"name"`
-	Values      []string   `db:"values"`
-	Position    int8       `db:"position"`
-	CreatedAt   time.Time  `db:"created_at"`
-	UpdatedAt   time.Time  `db:"updated_at"`
-	PublishedAt *time.Time `db:"published_at"`
-	DeletedAt   *time.Time `db:"deleted_at"`
+	Id            string     `db:"id"`
+	Name          string     `db:"name"`
+	SelectedValue string     `db:"value"`
+	Values        []string   `db:"values"`
+	Position      int8       `db:"position"`
+	CreatedAt     time.Time  `db:"created_at"`
+	UpdatedAt     time.Time  `db:"updated_at"`
+	PublishedAt   *time.Time `db:"published_at"`
+	DeletedAt     *time.Time `db:"deleted_at"`
 }
 
 func (opt *ProductOption) As() *generated.ProductOption {
@@ -24,3 +25,10 @@ func (opt *ProductOption) As() *generated.ProductOption {
 		Values: opt.Values,
 	}
 }
+
+func (opt *ProductOption) AsSelected() *generated.SelectedOption {
+	return &generated.SelectedOption{
+		Name:  opt.Name,
+		Value: opt.SelectedValue,
+	}
+}

+ 14 - 20
relation/product_variant.go

@@ -25,17 +25,12 @@ type ProductVariant struct {
 	UpdatedAt           time.Time       `db:"updated_at"`
 	PublishedAt         *time.Time      `db:"published_at"`
 	DeletedAt           *time.Time      `db:"deleted_at"`
-	Options             []struct {
-		Field0 string
-		Field1 int32
-	} `db:"options"`
-
-	Title             string         `db:"title"`
-	Sku               dbr.NullString `db:"sku"`
-	Barcode           dbr.NullString `db:"barcode"`
-	RequiresShipping  bool           `db:"requires_shipping"`
-	QuantityAvailable int            `db:"available"`
-	ForSale           bool           `db:"for_sale"`
+	Title               string          `db:"title"`
+	Sku                 dbr.NullString  `db:"sku"`
+	Barcode             dbr.NullString  `db:"barcode"`
+	RequiresShipping    bool            `db:"requires_shipping"`
+	QuantityAvailable   int             `db:"available"`
+	ForSale             bool            `db:"for_sale"`
 }
 
 func (v *ProductVariant) As() *generated.ProductVariant {
@@ -43,21 +38,20 @@ func (v *ProductVariant) As() *generated.ProductVariant {
 		AvailableForSale:    v.ForSale,
 		CurrentlyNotInStock: v.QuantityAvailable <= 0,
 
-		Image:                nil,
-		SelectedOptions:      nil,
-		UnitPrice:            nil,
-		UnitPriceMeasurement: nil,
+		Image:                nil, //TODO:
+		UnitPrice:            nil, //TODO:
+		UnitPriceMeasurement: nil, //TODO:
 
+		Price: &generated.MoneyV2{
+			Amount:       scalar.NewDecimal(v.Price),
+			CurrencyCode: generated.CurrencyCodeUsd,
+		},
 		CompareAtPrice: &generated.MoneyV2{
 			Amount:       scalar.NewDecimal(v.CompareAtPrice),
 			CurrencyCode: generated.CurrencyCodeUsd,
 		},
 
-		ID: model.NewId(model.GidVariant, v.Id),
-		Price: &generated.MoneyV2{
-			Amount:       scalar.NewDecimal(v.Price),
-			CurrencyCode: generated.CurrencyCodeUsd,
-		},
+		ID:                model.NewId(model.GidVariant, v.Id),
 		Product:           &generated.Product{ID: model.NewId(model.GidProduct, v.ProductId)},
 		QuantityAvailable: &v.QuantityAvailable,
 		RequiresShipping:  v.RequiresShipping,