From c5fecbdbf8a3963d3a2f1955511af99dfd3ffd9a Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Wed, 5 Mar 2025 11:50:44 +0000 Subject: [PATCH 01/12] WIP Craft GQL --- src/Plugin.php | 52 +++++++++++++++++ src/elements/Product.php | 18 ++++++ src/gql/arguments/elements/Product.php | 55 ++++++++++++++++++ src/gql/interfaces/elements/Product.php | 74 ++++++++++++++++++++++++ src/gql/queries/Product.php | 57 ++++++++++++++++++ src/gql/resolvers/elements/Product.php | 61 +++++++++++++++++++ src/gql/types/elements/Product.php | 32 ++++++++++ src/gql/types/generators/ProductType.php | 53 +++++++++++++++++ src/translations/en/shopify.php | 1 + 9 files changed, 403 insertions(+) create mode 100644 src/gql/arguments/elements/Product.php create mode 100644 src/gql/interfaces/elements/Product.php create mode 100644 src/gql/queries/Product.php create mode 100644 src/gql/resolvers/elements/Product.php create mode 100644 src/gql/types/elements/Product.php create mode 100644 src/gql/types/generators/ProductType.php diff --git a/src/Plugin.php b/src/Plugin.php index 026d755..87ea185 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -19,6 +19,9 @@ use craft\db\Query; use craft\events\DefineConsoleActionsEvent; use craft\events\RegisterComponentTypesEvent; +use craft\events\RegisterGqlQueriesEvent; +use craft\events\RegisterGqlSchemaComponentsEvent; +use craft\events\RegisterGqlTypesEvent; use craft\events\RegisterUrlRulesEvent; use craft\feedme\events\RegisterFeedMeFieldsEvent; use craft\fields\Link; @@ -28,11 +31,14 @@ use craft\services\Elements; use craft\services\Fields; use craft\services\Gc; +use craft\services\Gql; use craft\services\Utilities; use craft\shopify\db\Table; use craft\shopify\elements\Product; use craft\shopify\feedme\fields\Products as FeedMeProductsField; use craft\shopify\fields\Products as ProductsField; +use craft\shopify\gql\interfaces\elements\Product as GqlProductInterface; +use craft\shopify\gql\queries\Product as GqlProductQueries; use craft\shopify\handlers\Webhook; use craft\shopify\linktypes\Product as ProductLinkType; use craft\shopify\models\Settings; @@ -130,6 +136,9 @@ public function init() $this->_registerResaveCommands(); $this->_registerGarbageCollection(); $this->_registerFeedMeEvents(); + $this->_registerGqlInterfaces(); + $this->_registerGqlQueries(); + $this->_registerGqlComponents(); if (!$request->getIsConsoleRequest()) { if ($request->getIsCpRequest()) { @@ -244,6 +253,49 @@ private function _registerElementTypes(): void }); } + + /** + * Register the Gql interfaces + * @since 6.1.0 + */ + private function _registerGqlInterfaces(): void + { + Event::on(Gql::class, Gql::EVENT_REGISTER_GQL_TYPES, static function(RegisterGqlTypesEvent $event) { + $event->types[] = GqlProductInterface::class; + }); + } + + /** + * Register the Gql queries + * @since 6.1.0 + */ + private function _registerGqlQueries(): void + { + Event::on(Gql::class, Gql::EVENT_REGISTER_GQL_QUERIES, static function(RegisterGqlQueriesEvent $event) { + $event->queries = array_merge( + $event->queries, + GqlProductQueries::getQueries(), + ); + }); + } + + /** + * Register the Gql permissions + * @since 6.1.0 + */ + private function _registerGqlComponents(): void + { + Event::on(Gql::class, Gql::EVENT_REGISTER_GQL_SCHEMA_COMPONENTS, static function(RegisterGqlSchemaComponentsEvent $event) { + $typeName = (new Product())->getGqlTypeName(); + $event->queries = array_merge($event->queries, [ + Craft::t('shopify', 'Shopify Products') => [ + $typeName . ':read' => ['label' => Craft::t('shopify', 'View products')], + ] + ]); + }); + } + + /** * Register Shopify’s fields * diff --git a/src/elements/Product.php b/src/elements/Product.php index efdd9a6..a84d662 100644 --- a/src/elements/Product.php +++ b/src/elements/Product.php @@ -213,6 +213,24 @@ public function init(): void parent::init(); } + /** + * @inheritdoc + * @since 3.0 + */ + public static function gqlScopesByContext(mixed $context): array + { + /** @var FieldLayout $context */ + return ['ShopifyProduct']; + } + + /** + * @return string + */ + public function getGqlTypeName(): string + { + return 'ShopifyProduct'; + } + /** * @inheritdoc */ diff --git a/src/gql/arguments/elements/Product.php b/src/gql/arguments/elements/Product.php new file mode 100644 index 0000000..8c1998a --- /dev/null +++ b/src/gql/arguments/elements/Product.php @@ -0,0 +1,55 @@ + + * @since 6.1.0 + */ +class Product extends ElementArguments +{ + /** + * @inheritdoc + */ + public static function getArguments(): array + { + return array_merge(parent::getArguments(), self::getContentArguments(), [ + 'shopifyId' => [ + 'name' => 'shopifyId', + 'type' => Type::listOf(QueryArgument::getType()), + 'description' => 'Narrows the query results based on the Shopify ID on the product.', + ], + 'shopifyGid' => [ + 'name' => 'shopifyGid', + 'type' => Type::listOf(QueryArgument::getType()), + 'description' => 'Narrows the query results based on the Shopify GID on the product.', + ], + ]); + } + + /** + * @inheritdoc + */ + public static function getContentArguments(): array + { + $productFieldsArguments = Craft::$app->getGql()->getContentArguments([ + Plugin::getInstance()->getSettings()->getProductFieldLayout(), + ], ProductElement::class); + + return array_merge(parent::getContentArguments(), $productFieldsArguments); + } +} diff --git a/src/gql/interfaces/elements/Product.php b/src/gql/interfaces/elements/Product.php new file mode 100644 index 0000000..963b110 --- /dev/null +++ b/src/gql/interfaces/elements/Product.php @@ -0,0 +1,74 @@ + + * @since 6.1.0 + */ +class Product extends Element +{ + /** + * @inheritdoc + */ + public static function getTypeGenerator(): string + { + return ProductType::class; + } + + /** + * @inheritdoc + */ + public static function getType($fields = null): Type + { + if ($type = GqlEntityRegistry::getEntity(self::getName())) { + return $type; + } + + $type = GqlEntityRegistry::createEntity(self::getName(), new InterfaceType([ + 'name' => static::getName(), + 'fields' => self::class . '::getFieldDefinitions', + 'description' => 'This is the interface implemented by all products.', + 'resolveType' => function(ProductElement $value) { + return $value->getGqlTypeName(); + }, + ])); + + ProductType::generateTypes(); + + return $type; + } + + /** + * @inheritdoc + */ + public static function getName(): string + { + return 'ShopifyProductInterface'; + } + + /** + * @inheritdoc + */ + public static function getFieldDefinitions(): array + { + return Craft::$app->getGql()->prepareFieldDefinitions(array_merge(parent::getFieldDefinitions(), [ + + ]), self::getName()); + } +} diff --git a/src/gql/queries/Product.php b/src/gql/queries/Product.php new file mode 100644 index 0000000..6c549c2 --- /dev/null +++ b/src/gql/queries/Product.php @@ -0,0 +1,57 @@ + + * @since 6.1.0 + */ +class Product extends Query +{ + /** + * @inheritdoc + */ + public static function getQueries(bool $checkToken = true): array + { + $entities = Gql::extractAllowedEntitiesFromSchema(); + $typeName = (new \craft\shopify\elements\Product())->getGqlTypeName(); + if ($checkToken && !isset($entities[$typeName])) { + return []; + } + + return [ + 'shopifyProducts' => [ + 'type' => Type::listOf(ProductInterface::getType()), + 'args' => ProductArguments::getArguments(), + 'resolve' => ProductResolver::class . '::resolve', + 'description' => 'This query is used to query for products.', + ], + 'shopifyProductCount' => [ + 'type' => Type::nonNull(Type::int()), + 'args' => ProductArguments::getArguments(), + 'resolve' => ProductResolver::class . '::resolveCount', + 'description' => 'This query is used to return the number of products.', + ], + 'shopifyProduct' => [ + 'type' => ProductInterface::getType(), + 'args' => ProductArguments::getArguments(), + 'resolve' => ProductResolver::class . '::resolveOne', + 'description' => 'This query is used to query for a product.', + ], + ]; + } +} diff --git a/src/gql/resolvers/elements/Product.php b/src/gql/resolvers/elements/Product.php new file mode 100644 index 0000000..3834026 --- /dev/null +++ b/src/gql/resolvers/elements/Product.php @@ -0,0 +1,61 @@ + + * @since 6.1.0 + */ +class Product extends ElementResolver +{ + /** + * @inheritdoc + */ + public static function prepareQuery(mixed $source, array $arguments, $fieldName = null): mixed + { + // If this is the beginning of a resolver chain, start fresh + if ($source === null) { + $query = ProductElement::find(); + // If not, get the prepared element query + } else { + $query = $source->$fieldName; + } + + // If it's preloaded, it's preloaded. + if (!$query instanceof ElementQuery) { + return $query; + } + + foreach ($arguments as $key => $value) { + if (method_exists($query, $key)) { + $query->$key($value); + } elseif (property_exists($query, $key)) { + $query->$key = $value; + } else { + // Catch custom field queries + $query->$key($value); + } + } + + $pairs = GqlHelper::extractAllowedEntitiesFromSchema(); + $typeName = (new ProductElement())->getGqlTypeName(); + + if (!isset($pairs[$typeName])) { + return []; + } + + return $query; + } +} diff --git a/src/gql/types/elements/Product.php b/src/gql/types/elements/Product.php new file mode 100644 index 0000000..8cda7d4 --- /dev/null +++ b/src/gql/types/elements/Product.php @@ -0,0 +1,32 @@ + + * @since 6.1.0 + */ +class Product extends ElementType +{ + /** + * @inheritdoc + */ + public function __construct(array $config) + { + $config['interfaces'] = [ + ProductInterface::getType(), + ]; + + parent::__construct($config); + } +} diff --git a/src/gql/types/generators/ProductType.php b/src/gql/types/generators/ProductType.php new file mode 100644 index 0000000..4271dc6 --- /dev/null +++ b/src/gql/types/generators/ProductType.php @@ -0,0 +1,53 @@ + + * @since 6.1.0 + */ +class ProductType implements GeneratorInterface +{ + /** + * @inheritdoc + */ + public static function generateTypes(mixed $context = null): array + { + $productFieldLayout = Plugin::getInstance()->getSettings()->getProductFieldLayout(); + + $typeName = (new ProductElement())->getGqlTypeName(); + $contentFields = $productFieldLayout->getCustomFields(); + $contentFieldGqlTypes = []; + + /** @var Field $contentField */ + foreach ($contentFields as $contentField) { + $contentFieldGqlTypes[$contentField->handle] = $contentField->getContentGqlType(); + } + + $productFields = Craft::$app->getGql()->prepareFieldDefinitions(array_merge(ProductInterface::getFieldDefinitions(), $contentFieldGqlTypes), $typeName); + return [ + $typeName => GqlEntityRegistry::getEntity($typeName) ?: GqlEntityRegistry::createEntity($typeName, new ProductTypeElement([ + 'name' => $typeName, + 'fields' => function() use ($productFields) { + return $productFields; + }, + ])) + ]; + } +} diff --git a/src/translations/en/shopify.php b/src/translations/en/shopify.php index 2acd171..c957c20 100644 --- a/src/translations/en/shopify.php +++ b/src/translations/en/shopify.php @@ -82,6 +82,7 @@ 'Updating product variants for “{title}”' => 'Updating product variants for “{title}”', 'Variants' => 'Variants', 'Vendor' => 'Vendor', + 'View products' => 'View products', 'Webhook deleted' => 'Webhook deleted', 'Webhooks could not be deleted' => 'Webhooks could not be deleted', 'Webhooks could not be registered.' => 'Webhooks could not be registered.', From 05abd1413c8573499f2c6d937597bf170058b185 Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Wed, 5 Mar 2025 11:51:34 +0000 Subject: [PATCH 02/12] fix cs --- src/Plugin.php | 2 +- src/gql/arguments/elements/Product.php | 4 ++-- src/gql/interfaces/elements/Product.php | 4 ++-- src/gql/queries/Product.php | 2 +- src/gql/resolvers/elements/Product.php | 4 ++-- src/gql/types/elements/Product.php | 2 +- src/gql/types/generators/ProductType.php | 6 +++--- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Plugin.php b/src/Plugin.php index 87ea185..72b59c1 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -290,7 +290,7 @@ private function _registerGqlComponents(): void $event->queries = array_merge($event->queries, [ Craft::t('shopify', 'Shopify Products') => [ $typeName . ':read' => ['label' => Craft::t('shopify', 'View products')], - ] + ], ]); }); } diff --git a/src/gql/arguments/elements/Product.php b/src/gql/arguments/elements/Product.php index 8c1998a..cd59b7a 100644 --- a/src/gql/arguments/elements/Product.php +++ b/src/gql/arguments/elements/Product.php @@ -8,10 +8,10 @@ namespace craft\shopify\gql\arguments\elements; use Craft; -use craft\shopify\elements\Product as ProductElement; -use craft\shopify\Plugin; use craft\gql\base\ElementArguments; use craft\gql\types\QueryArgument; +use craft\shopify\elements\Product as ProductElement; +use craft\shopify\Plugin; use GraphQL\Type\Definition\Type; /** diff --git a/src/gql/interfaces/elements/Product.php b/src/gql/interfaces/elements/Product.php index 963b110..9cdee2b 100644 --- a/src/gql/interfaces/elements/Product.php +++ b/src/gql/interfaces/elements/Product.php @@ -8,10 +8,10 @@ namespace craft\shopify\gql\interfaces\elements; use Craft; -use craft\shopify\elements\Product as ProductElement; -use craft\shopify\gql\types\generators\ProductType; use craft\gql\GqlEntityRegistry; use craft\gql\interfaces\Element; +use craft\shopify\elements\Product as ProductElement; +use craft\shopify\gql\types\generators\ProductType; use GraphQL\Type\Definition\InterfaceType; use GraphQL\Type\Definition\Type; diff --git a/src/gql/queries/Product.php b/src/gql/queries/Product.php index 6c549c2..f7e0411 100644 --- a/src/gql/queries/Product.php +++ b/src/gql/queries/Product.php @@ -7,11 +7,11 @@ namespace craft\shopify\gql\queries; +use craft\gql\base\Query; use craft\helpers\Gql; use craft\shopify\gql\arguments\elements\Product as ProductArguments; use craft\shopify\gql\interfaces\elements\Product as ProductInterface; use craft\shopify\gql\resolvers\elements\Product as ProductResolver; -use craft\gql\base\Query; use GraphQL\Type\Definition\Type; /** diff --git a/src/gql/resolvers/elements/Product.php b/src/gql/resolvers/elements/Product.php index 3834026..b7e25c3 100644 --- a/src/gql/resolvers/elements/Product.php +++ b/src/gql/resolvers/elements/Product.php @@ -7,10 +7,10 @@ namespace craft\shopify\gql\resolvers\elements; -use craft\shopify\elements\Product as ProductElement; -use craft\helpers\Gql as GqlHelper; use craft\elements\db\ElementQuery; use craft\gql\base\ElementResolver; +use craft\helpers\Gql as GqlHelper; +use craft\shopify\elements\Product as ProductElement; /** * Class Product diff --git a/src/gql/types/elements/Product.php b/src/gql/types/elements/Product.php index 8cda7d4..1204c51 100644 --- a/src/gql/types/elements/Product.php +++ b/src/gql/types/elements/Product.php @@ -7,8 +7,8 @@ namespace craft\shopify\gql\types\elements; -use craft\shopify\gql\interfaces\elements\Product as ProductInterface; use craft\gql\types\elements\Element as ElementType; +use craft\shopify\gql\interfaces\elements\Product as ProductInterface; /** * Class Product diff --git a/src/gql/types/generators/ProductType.php b/src/gql/types/generators/ProductType.php index 4271dc6..eefd8e3 100644 --- a/src/gql/types/generators/ProductType.php +++ b/src/gql/types/generators/ProductType.php @@ -9,11 +9,11 @@ use Craft; use craft\base\Field; +use craft\gql\base\GeneratorInterface; +use craft\gql\GqlEntityRegistry; use craft\shopify\elements\Product as ProductElement; use craft\shopify\gql\interfaces\elements\Product as ProductInterface; use craft\shopify\gql\types\elements\Product as ProductTypeElement; -use craft\gql\base\GeneratorInterface; -use craft\gql\GqlEntityRegistry; use craft\shopify\Plugin; /** @@ -47,7 +47,7 @@ public static function generateTypes(mixed $context = null): array 'fields' => function() use ($productFields) { return $productFields; }, - ])) + ])), ]; } } From c37d9f29f1d56517edaea1180baa646618305edd Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Wed, 5 Mar 2025 12:10:52 +0000 Subject: [PATCH 03/12] Add extra gql product arguments --- src/elements/db/ProductQuery.php | 1 - src/gql/arguments/elements/Product.php | 30 ++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/elements/db/ProductQuery.php b/src/elements/db/ProductQuery.php index afa18c0..c5ddafe 100644 --- a/src/elements/db/ProductQuery.php +++ b/src/elements/db/ProductQuery.php @@ -46,7 +46,6 @@ class ProductQuery extends ElementQuery */ public mixed $handle = null; - /** * @var mixed|null */ diff --git a/src/gql/arguments/elements/Product.php b/src/gql/arguments/elements/Product.php index cd59b7a..c5a7bf6 100644 --- a/src/gql/arguments/elements/Product.php +++ b/src/gql/arguments/elements/Product.php @@ -38,6 +38,36 @@ public static function getArguments(): array 'type' => Type::listOf(QueryArgument::getType()), 'description' => 'Narrows the query results based on the Shopify GID on the product.', ], + 'shopifyStatus' => [ + 'name' => 'shopifyStatus', + 'type' => Type::listOf(QueryArgument::getType()), + 'description' => 'Narrows the query results based on the Shopify status of the product.', + ], + 'handle' => [ + 'name' => 'handle', + 'type' => Type::listOf(QueryArgument::getType()), + 'description' => 'Narrows the query results based on the handle on the product.', + ], + 'productType' => [ + 'name' => 'productType', + 'type' => Type::listOf(QueryArgument::getType()), + 'description' => 'Narrows the query results based on the product type on the product.', + ], + 'tags' => [ + 'name' => 'tags', + 'type' => Type::listOf(QueryArgument::getType()), + 'description' => 'Narrows the query results based on the tags on the product.', + ], + 'vendor' => [ + 'name' => 'vendor', + 'type' => Type::listOf(QueryArgument::getType()), + 'description' => 'Narrows the query results based on the vendor on the product.', + ], + 'publishedOnCurrentPublication' => [ + 'name' => 'publishedOnCurrentPublication', + 'type' => Type::boolean(), + 'description' => 'Narrows the query results based on the published on current publication on the product.', + ], ]); } From ebe0de39c6c82df9fdf9043d6556fb3b495a21b2 Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Mon, 10 Mar 2025 08:05:08 +0000 Subject: [PATCH 04/12] Make sure to eagleload non-elements in product queries --- src/gql/resolvers/elements/Product.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/gql/resolvers/elements/Product.php b/src/gql/resolvers/elements/Product.php index b7e25c3..29a5fd2 100644 --- a/src/gql/resolvers/elements/Product.php +++ b/src/gql/resolvers/elements/Product.php @@ -56,6 +56,8 @@ public static function prepareQuery(mixed $source, array $arguments, $fieldName return []; } + $query->withAll(); + return $query; } } From b8aee2b3dd9d247308e75fc6689ff5e89037790d Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Mon, 10 Mar 2025 08:05:50 +0000 Subject: [PATCH 05/12] Add variants to product gql fields --- src/gql/interfaces/elements/Product.php | 7 ++- src/gql/types/VariantType.php | 72 +++++++++++++++++++++++++ src/gql/types/elements/Product.php | 11 ++++ 3 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 src/gql/types/VariantType.php diff --git a/src/gql/interfaces/elements/Product.php b/src/gql/interfaces/elements/Product.php index 9cdee2b..5b24d5c 100644 --- a/src/gql/interfaces/elements/Product.php +++ b/src/gql/interfaces/elements/Product.php @@ -12,6 +12,7 @@ use craft\gql\interfaces\Element; use craft\shopify\elements\Product as ProductElement; use craft\shopify\gql\types\generators\ProductType; +use craft\shopify\gql\types\VariantType; use GraphQL\Type\Definition\InterfaceType; use GraphQL\Type\Definition\Type; @@ -68,7 +69,11 @@ public static function getName(): string public static function getFieldDefinitions(): array { return Craft::$app->getGql()->prepareFieldDefinitions(array_merge(parent::getFieldDefinitions(), [ - + 'variants' => [ + 'name' => 'variants', + 'type' => VariantType::getType(), + 'description' => 'The product’s variants.', + ], ]), self::getName()); } } diff --git a/src/gql/types/VariantType.php b/src/gql/types/VariantType.php new file mode 100644 index 0000000..10ddc32 --- /dev/null +++ b/src/gql/types/VariantType.php @@ -0,0 +1,72 @@ + + * @since 6.1.0 + */ +class VariantType extends ScalarType implements SingularTypeInterface +{ + /** + * @return string + */ + public static function getName(): string + { + return 'ShopifyVariant'; + } + + public static function getType(): Type + { + return GqlEntityRegistry::getOrCreate(static::getName(), fn() => new self()); + } + + public function serialize($value) + { + if (empty($value)) { + return null; + } + + return $value; + } + + public function parseValue($value) + { + if (!is_string($value) && !is_array($value) && !is_null($value)) { + throw new GqlException('Variants must be either a string, array, or null.'); + } + + return $value; + } + + public function parseLiteral(Node $valueNode, ?array $variables = null) + { + if ($valueNode instanceof StringValueNode) { + return Json::decodeIfJson($valueNode->value); + } + + if ($valueNode instanceof NullValueNode) { + return null; + } + + // This message will be lost by the wrapping exception, but it feels good to provide one. + throw new GqlException("Variants must be either an array, string or null."); + } +} diff --git a/src/gql/types/elements/Product.php b/src/gql/types/elements/Product.php index 1204c51..02db353 100644 --- a/src/gql/types/elements/Product.php +++ b/src/gql/types/elements/Product.php @@ -9,6 +9,7 @@ use craft\gql\types\elements\Element as ElementType; use craft\shopify\gql\interfaces\elements\Product as ProductInterface; +use GraphQL\Type\Definition\ResolveInfo; /** * Class Product @@ -29,4 +30,14 @@ public function __construct(array $config) parent::__construct($config); } + + protected function resolve(mixed $source, array $arguments, mixed $context, ResolveInfo $resolveInfo): mixed + { + $fieldName = $resolveInfo->fieldName; + return match ($fieldName) { + // @TODO remove this when the conflict in (https://github.com/craftcms/cms/blob/7b889521442ef68be39edf52b55f4747da722e94/src/gql/ElementQueryConditionBuilder.php#L290-L321) is resolved + 'variants' => $source->getVariants(), + default => parent::resolve($source, $arguments, $context, $resolveInfo), + }; + } } From 9a8321b152a9d1c2a781652ba86bcf6083809640 Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Tue, 11 Mar 2025 09:33:31 +0000 Subject: [PATCH 06/12] Add more GQL product field definitions --- src/gql/interfaces/elements/Product.php | 56 +++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/src/gql/interfaces/elements/Product.php b/src/gql/interfaces/elements/Product.php index 5b24d5c..a806412 100644 --- a/src/gql/interfaces/elements/Product.php +++ b/src/gql/interfaces/elements/Product.php @@ -10,6 +10,7 @@ use Craft; use craft\gql\GqlEntityRegistry; use craft\gql\interfaces\Element; +use craft\gql\types\DateTime; use craft\shopify\elements\Product as ProductElement; use craft\shopify\gql\types\generators\ProductType; use craft\shopify\gql\types\VariantType; @@ -69,6 +70,61 @@ public static function getName(): string public static function getFieldDefinitions(): array { return Craft::$app->getGql()->prepareFieldDefinitions(array_merge(parent::getFieldDefinitions(), [ + 'createdAt' => [ + 'name' => 'createdAt', + 'type' => DateTime::getType(), + 'description' => 'The date the product was created in Shopify.', + ], + 'publishedAt' => [ + 'name' => 'publishedAt', + 'type' => DateTime::getType(), + 'description' => 'The date the product was published in Shopify.', + ], + 'updatedAt' => [ + 'name' => 'updatedAt', + 'type' => DateTime::getType(), + 'description' => 'The date the product was updated in Shopify.', + ], + 'handle' => [ + 'name' => 'handle', + 'type' => Type::string(), + 'description' => 'The product’s handle.', + ], + 'descriptionHtml' => [ + 'name' => 'descriptionHtml', + 'type' => Type::string(), + 'description' => 'The product’s description HTML in Shopify.', + ], + 'productType' => [ + 'name' => 'productType', + 'type' => Type::string(), + 'description' => 'The product’s type in Shopify.', + ], + 'publishedOnCurrentPublication' => [ + 'name' => 'publishedOnCurrentPublication', + 'type' => Type::boolean(), + 'description' => 'If the product is published on the current publication in Shopify.', + ], + 'shopifyId' => [ + 'name' => 'shopifyId', + 'type' => Type::string(), + 'description' => 'The product’s Shopify ID.', + ], + 'shopifyGid' => [ + 'name' => 'shopifyGid', + 'type' => Type::string(), + 'description' => 'The product’s Shopify GID.', + ], + 'shopifyStatus' => [ + 'name' => 'shopifyStatus', + 'type' => Type::string(), + 'description' => 'The product’s status in Shopify.', + ], + 'vendor' => [ + 'name' => 'vendor', + 'type' => Type::string(), + 'description' => 'The product’s vendor in Shopify.', + ], 'variants' => [ 'name' => 'variants', 'type' => VariantType::getType(), From 729632b12aecf5cabe3720a3407d1e6d1ccd319f Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Tue, 11 Mar 2025 09:39:01 +0000 Subject: [PATCH 07/12] Favour a more generic type that is resuable --- src/gql/interfaces/elements/Product.php | 19 ++++++- src/gql/types/JsonType.php | 72 +++++++++++++++++++++++++ src/gql/types/VariantType.php | 2 +- 3 files changed, 90 insertions(+), 3 deletions(-) create mode 100644 src/gql/types/JsonType.php diff --git a/src/gql/interfaces/elements/Product.php b/src/gql/interfaces/elements/Product.php index a806412..7b85b9d 100644 --- a/src/gql/interfaces/elements/Product.php +++ b/src/gql/interfaces/elements/Product.php @@ -13,7 +13,7 @@ use craft\gql\types\DateTime; use craft\shopify\elements\Product as ProductElement; use craft\shopify\gql\types\generators\ProductType; -use craft\shopify\gql\types\VariantType; +use craft\shopify\gql\types\JsonType; use GraphQL\Type\Definition\InterfaceType; use GraphQL\Type\Definition\Type; @@ -125,9 +125,24 @@ public static function getFieldDefinitions(): array 'type' => Type::string(), 'description' => 'The product’s vendor in Shopify.', ], + 'images' => [ + 'name' => 'images', + 'type' => JsonType::getType(), + 'description' => 'The product’s images in Shopify.', + ], + 'tags' => [ + 'name' => 'tags', + 'type' => JsonType::getType(), + 'description' => 'The product’s tags in Shopify.', + ], + 'metafields' => [ + 'name' => 'metafields', + 'type' => JsonType::getType(), + 'description' => 'The product’s metafields in Shopify.', + ], 'variants' => [ 'name' => 'variants', - 'type' => VariantType::getType(), + 'type' => JsonType::getType(), 'description' => 'The product’s variants.', ], ]), self::getName()); diff --git a/src/gql/types/JsonType.php b/src/gql/types/JsonType.php new file mode 100644 index 0000000..2720e51 --- /dev/null +++ b/src/gql/types/JsonType.php @@ -0,0 +1,72 @@ + + * @since 6.1.0 + */ +class JsonType extends ScalarType implements SingularTypeInterface +{ + /** + * @return string + */ + public static function getName(): string + { + return 'ShopifyJson'; + } + + public static function getType(): Type + { + return GqlEntityRegistry::getOrCreate(static::getName(), fn() => new self()); + } + + public function serialize($value) + { + if (empty($value)) { + return null; + } + + return $value; + } + + public function parseValue($value) + { + if (!is_string($value) && !is_array($value) && !is_null($value)) { + throw new GqlException('Data must be either a string, array, or null.'); + } + + return $value; + } + + public function parseLiteral(Node $valueNode, ?array $variables = null) + { + if ($valueNode instanceof StringValueNode) { + return Json::decodeIfJson($valueNode->value); + } + + if ($valueNode instanceof NullValueNode) { + return null; + } + + // This message will be lost by the wrapping exception, but it feels good to provide one. + throw new GqlException("Data must be either an array, string or null."); + } +} diff --git a/src/gql/types/VariantType.php b/src/gql/types/VariantType.php index 10ddc32..2aeacd0 100644 --- a/src/gql/types/VariantType.php +++ b/src/gql/types/VariantType.php @@ -18,7 +18,7 @@ use GraphQL\Type\Definition\Type; /** - * Class SaleType + * Class VariantType * * @author Pixel & Tonic, Inc. * @since 6.1.0 From 54426f8762af2993c462ab96ba0d2080654aae99 Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Tue, 11 Mar 2025 09:40:06 +0000 Subject: [PATCH 08/12] Allow access to the full data --- src/gql/interfaces/elements/Product.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/gql/interfaces/elements/Product.php b/src/gql/interfaces/elements/Product.php index 7b85b9d..1fd14fb 100644 --- a/src/gql/interfaces/elements/Product.php +++ b/src/gql/interfaces/elements/Product.php @@ -143,7 +143,12 @@ public static function getFieldDefinitions(): array 'variants' => [ 'name' => 'variants', 'type' => JsonType::getType(), - 'description' => 'The product’s variants.', + 'description' => 'The product’s variants in Shopify.', + ], + 'data' => [ + 'name' => 'data', + 'type' => JsonType::getType(), + 'description' => 'The product’s synced data from Shopify.', ], ]), self::getName()); } From 0c7d7eda96e7d0159b81510be1dbe27850a06346 Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Tue, 11 Mar 2025 09:40:30 +0000 Subject: [PATCH 09/12] `VariantType` is no longer in use --- src/gql/types/VariantType.php | 72 ----------------------------------- 1 file changed, 72 deletions(-) delete mode 100644 src/gql/types/VariantType.php diff --git a/src/gql/types/VariantType.php b/src/gql/types/VariantType.php deleted file mode 100644 index 2aeacd0..0000000 --- a/src/gql/types/VariantType.php +++ /dev/null @@ -1,72 +0,0 @@ - - * @since 6.1.0 - */ -class VariantType extends ScalarType implements SingularTypeInterface -{ - /** - * @return string - */ - public static function getName(): string - { - return 'ShopifyVariant'; - } - - public static function getType(): Type - { - return GqlEntityRegistry::getOrCreate(static::getName(), fn() => new self()); - } - - public function serialize($value) - { - if (empty($value)) { - return null; - } - - return $value; - } - - public function parseValue($value) - { - if (!is_string($value) && !is_array($value) && !is_null($value)) { - throw new GqlException('Variants must be either a string, array, or null.'); - } - - return $value; - } - - public function parseLiteral(Node $valueNode, ?array $variables = null) - { - if ($valueNode instanceof StringValueNode) { - return Json::decodeIfJson($valueNode->value); - } - - if ($valueNode instanceof NullValueNode) { - return null; - } - - // This message will be lost by the wrapping exception, but it feels good to provide one. - throw new GqlException("Variants must be either an array, string or null."); - } -} From 994e29658d806d1ad42f4ca594d9ccafe4e2e964 Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Wed, 25 Mar 2026 09:22:08 +0000 Subject: [PATCH 10/12] Update @since tags for new GraphQL product support to 7.1.0 --- src/elements/Product.php | 3 ++- src/gql/arguments/elements/Product.php | 2 +- src/gql/interfaces/elements/Product.php | 2 +- src/gql/queries/Product.php | 2 +- src/gql/resolvers/elements/Product.php | 2 +- src/gql/types/JsonType.php | 2 +- src/gql/types/elements/Product.php | 2 +- src/gql/types/generators/ProductType.php | 2 +- 8 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/elements/Product.php b/src/elements/Product.php index f0ec34a..3430945 100644 --- a/src/elements/Product.php +++ b/src/elements/Product.php @@ -195,7 +195,7 @@ public function init(): void /** * @inheritdoc - * @since 3.0 + * @since 7.1.0 */ public static function gqlScopesByContext(mixed $context): array { @@ -205,6 +205,7 @@ public static function gqlScopesByContext(mixed $context): array /** * @return string + * @since 7.1.0 */ public function getGqlTypeName(): string { diff --git a/src/gql/arguments/elements/Product.php b/src/gql/arguments/elements/Product.php index c5a7bf6..89e0125 100644 --- a/src/gql/arguments/elements/Product.php +++ b/src/gql/arguments/elements/Product.php @@ -18,7 +18,7 @@ * Class Product * * @author Pixel & Tonic, Inc. - * @since 6.1.0 + * @since 7.1.0 */ class Product extends ElementArguments { diff --git a/src/gql/interfaces/elements/Product.php b/src/gql/interfaces/elements/Product.php index 1fd14fb..a265905 100644 --- a/src/gql/interfaces/elements/Product.php +++ b/src/gql/interfaces/elements/Product.php @@ -21,7 +21,7 @@ * Class Product * * @author Pixel & Tonic, Inc. - * @since 6.1.0 + * @since 7.1.0 */ class Product extends Element { diff --git a/src/gql/queries/Product.php b/src/gql/queries/Product.php index f7e0411..5c1029a 100644 --- a/src/gql/queries/Product.php +++ b/src/gql/queries/Product.php @@ -18,7 +18,7 @@ * Class Product * * @author Pixel & Tonic, Inc. - * @since 6.1.0 + * @since 7.1.0 */ class Product extends Query { diff --git a/src/gql/resolvers/elements/Product.php b/src/gql/resolvers/elements/Product.php index 29a5fd2..6b62bde 100644 --- a/src/gql/resolvers/elements/Product.php +++ b/src/gql/resolvers/elements/Product.php @@ -16,7 +16,7 @@ * Class Product * * @author Pixel & Tonic, Inc. - * @since 6.1.0 + * @since 7.1.0 */ class Product extends ElementResolver { diff --git a/src/gql/types/JsonType.php b/src/gql/types/JsonType.php index 2720e51..9e5335c 100644 --- a/src/gql/types/JsonType.php +++ b/src/gql/types/JsonType.php @@ -21,7 +21,7 @@ * Class JsonType * * @author Pixel & Tonic, Inc. - * @since 6.1.0 + * @since 7.1.0 */ class JsonType extends ScalarType implements SingularTypeInterface { diff --git a/src/gql/types/elements/Product.php b/src/gql/types/elements/Product.php index 02db353..c3bafd5 100644 --- a/src/gql/types/elements/Product.php +++ b/src/gql/types/elements/Product.php @@ -15,7 +15,7 @@ * Class Product * * @author Pixel & Tonic, Inc. - * @since 6.1.0 + * @since 7.1.0 */ class Product extends ElementType { diff --git a/src/gql/types/generators/ProductType.php b/src/gql/types/generators/ProductType.php index eefd8e3..7aa3446 100644 --- a/src/gql/types/generators/ProductType.php +++ b/src/gql/types/generators/ProductType.php @@ -20,7 +20,7 @@ * Class ProductType * * @author Pixel & Tonic, Inc. - * @since 6.1.0 + * @since 7.1.0 */ class ProductType implements GeneratorInterface { From 6c9249e5c4ae1e9b8e8c8fe89925f25600119b81 Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Wed, 25 Mar 2026 12:08:48 +0000 Subject: [PATCH 11/12] tidy --- src/Plugin.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Plugin.php b/src/Plugin.php index 2317e67..f41460e 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -263,7 +263,7 @@ private function _registerElementTypes(): void /** * Register the Gql interfaces - * @since 6.1.0 + * @since 7.1.0 */ private function _registerGqlInterfaces(): void { @@ -274,7 +274,7 @@ private function _registerGqlInterfaces(): void /** * Register the Gql queries - * @since 6.1.0 + * @since 7.1.0 */ private function _registerGqlQueries(): void { @@ -288,7 +288,7 @@ private function _registerGqlQueries(): void /** * Register the Gql permissions - * @since 6.1.0 + * @since 7.1.0 */ private function _registerGqlComponents(): void { From 2c685a8b8c71a1260fa756f19d20d913fb2f5418 Mon Sep 17 00:00:00 2001 From: Nathaniel Hammond Date: Wed, 25 Mar 2026 12:28:56 +0000 Subject: [PATCH 12/12] No longer applicable --- src/gql/arguments/elements/Product.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/gql/arguments/elements/Product.php b/src/gql/arguments/elements/Product.php index 89e0125..21bfde8 100644 --- a/src/gql/arguments/elements/Product.php +++ b/src/gql/arguments/elements/Product.php @@ -63,11 +63,6 @@ public static function getArguments(): array 'type' => Type::listOf(QueryArgument::getType()), 'description' => 'Narrows the query results based on the vendor on the product.', ], - 'publishedOnCurrentPublication' => [ - 'name' => 'publishedOnCurrentPublication', - 'type' => Type::boolean(), - 'description' => 'Narrows the query results based on the published on current publication on the product.', - ], ]); }