Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
c5fecbd
WIP Craft GQL
nfourtythree Mar 5, 2025
05abd14
fix cs
nfourtythree Mar 5, 2025
c37d9f2
Add extra gql product arguments
nfourtythree Mar 5, 2025
ebe0de3
Make sure to eagleload non-elements in product queries
nfourtythree Mar 10, 2025
b8aee2b
Add variants to product gql fields
nfourtythree Mar 10, 2025
f4e6537
Merge branch '6.x' into feature/pt-2562-graphql-support
nfourtythree Mar 11, 2025
9a8321b
Add more GQL product field definitions
nfourtythree Mar 11, 2025
729632b
Favour a more generic type that is resuable
nfourtythree Mar 11, 2025
54426f8
Allow access to the full data
nfourtythree Mar 11, 2025
0c7d7ed
`VariantType` is no longer in use
nfourtythree Mar 11, 2025
d930203
Merge branch '7.x' into feature/pt-2562-graphql-support
nfourtythree Mar 25, 2026
994e296
Update @since tags for new GraphQL product support to 7.1.0
nfourtythree Mar 25, 2026
6c9249e
tidy
nfourtythree Mar 25, 2026
638d959
Merge branch '7.1' into feature/pt-2562-graphql-support
nfourtythree Mar 25, 2026
2c685a8
No longer applicable
nfourtythree Mar 25, 2026
d77b20b
no longer applicable
nfourtythree Mar 25, 2026
8f9216a
Shopify Products GQL querying
nfourtythree Mar 30, 2026
e171e71
Add returning of product element data (and querying) from product cus…
nfourtythree Mar 31, 2026
cf32ad6
PHPstan fix
nfourtythree Mar 31, 2026
f2db62a
Fix `totalInventory` access
AugustMiller Mar 31, 2026
b6a4980
Add `totalInventory` to product element and gql queries
nfourtythree Apr 1, 2026
fae27dd
Merge branch 'feature/pt-2562-graphql-support' of github.com:craftcms…
nfourtythree Apr 1, 2026
c63216e
Revert "Fix `totalInventory` access"
nfourtythree Apr 1, 2026
06a75a5
Tidy product type GQL generation
nfourtythree Apr 1, 2026
9cd8c13
Add general GraphQL note to changelog, separate into categories
AugustMiller Apr 2, 2026
daea7ef
Add GraphQL documentation
AugustMiller Apr 2, 2026
5171248
Mention changelog in readme
AugustMiller Apr 2, 2026
226d7bb
Complete sentence in GQL docs
AugustMiller Apr 2, 2026
93395db
speling :)
AugustMiller Apr 2, 2026
ee5b98b
Fix postgres generated column
nfourtythree Apr 3, 2026
e89f215
Merge remote-tracking branch 'origin/feature/pt-2562-graphql-support'…
nfourtythree Apr 3, 2026
716d1e7
Use GQL_TYPE_NAME constant in gqlScopesByContext() and getGqlTypeName()
Copilot Apr 3, 2026
9c68c12
Make sure metafields are re-mapped with key and value props in gql
nfourtythree Apr 3, 2026
b903160
fix cs
nfourtythree Apr 3, 2026
33b1b45
Make sure metafield `value` is always being returned as a string for …
nfourtythree Apr 3, 2026
a7cbd61
Remove no longer need `url` remapping in `Image` GQL type
nfourtythree Apr 3, 2026
35fabb8
fix cs
nfourtythree Apr 3, 2026
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
18 changes: 17 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,23 @@

## WIP - 7.1.0

- TBC
### Development

- Product elements are now available via Craft’s own GraphQL API. ([#206](https://github.com/craftcms/shopify/pull/206))

### Extensibility

- Added the `totalInventory` product query param.
- Added `craft\shopify\elements\db\ProductQuery::$totalInventory`.
- Added `craft\shopify\elements\db\ProductQuery::totalInventory()`.
- Added `craft\shopify\elements\Product::$totalInventory`.
- Added `craft\shopify\gql\arguments\elements\Product`.
- Added `craft\shopify\gql\interfaces\elements\Product`.
- Added `craft\shopify\gql\types\elements\Product`.
- Added `craft\shopify\gql\types\Image`.
- Added `craft\shopify\gql\types\Metafield`.
- Added `craft\shopify\gql\types\Option`.
- Added `craft\shopify\gql\types\Variant`.

## 7.0.2 - 2026-03-24

Expand Down
116 changes: 115 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,12 +208,13 @@ Discover orphaned subscriptions using the [`webhookSubscriptions()`](https://sho

## Upgrading

This release (7.x) is primarily concerned with Shopify API compatability, but the [new authentication mechanism](#connect-to-shopify) means that you’ll need to re-establish the connection to Shopify using the authentication scheme [described above](#connect-to-shopify).
This version (7.x) is primarily concerned with Shopify API compatibility, but the [new authentication mechanism](#connect-to-shopify) means that you’ll need to re-establish the connection to Shopify using the authentication scheme [described above](#connect-to-shopify).

Due to significant shifts in Shopify’s developer ecosystem, many of the [front-end cart management](#front-end-sdks) techniques we have recommended (like the _JS Buy SDK_ and _Buy Button JS_) are no longer viable.

> [!TIP]
> We strongly recommend reviewing this same section on the [6.x](https://github.com/craftcms/shopify/blob/6.x/README.md#upgrading) branch, as there were a number of breaking changes and deprecations during the upgrade from 5.x.
> The [changelog](https://github.com/craftcms/shopify/blob/7.x/CHANGELOG.md) contains specific information about the classes and methods that have been added, removed, or deprecated.

After the upgrade, you **must** [delete and re-create](#set-up-webhooks) webhooks for each environment. Webhooks are registered and delivered with a specific version, and a mismatch will result in errors.

Expand Down Expand Up @@ -572,6 +573,119 @@ Filter by the vendor information from Shopify.

You can still access `product.variants`, `product.images`, and `product.metafields` without eager-loading—but it may result in an additional query for each kind of content. Once you’ve retrieved variants, for example, they are memoized on the product element instance for the duration of the request.

### GraphQL

Product elements are also exposed via [Craft’s GraphQL API](https://craftcms.com/docs/5.x/development/graphql.html).
You can fetch products (and any content added via custom fields) using the `shopifyProducts()` query:

```graphql
query PowerTools {
shopifyProducts(productType: "powertools") {
# Native Product element properties:
id
title
shopifyStatus
images {
image {
url
altText
width
height
}
}

# Custom fields, from the field layout in Craft:
... on ShopifyProduct {
brandName
brandFamily
}
}
}
```

Products can be retrieved one at a time with the `shopifyProduct` (singular) query.
You might use this in a headless front-end to resolve a product by its slug, based on your routing:

```graphql
query OneProductBySlug($slug: [String]) {
shopifyProduct(slug: $slug) {
id
title
# ...
}
}

# Variables:
# {
# slug: Astro.params.slug
# }
```

It’s important to note that the GraphQL schema is _not_ the same as directly accessing the Shopify API, and that the built-in documentation only reflects what is accessible via Craft.
You’ll encounter many _familiar_ objects, but only a subset of the types and fields are available.
Only the data retrieved during synchronization (plus your custom fields and other native element properties) will be present in the API.

Arguments are also radically different: Shopify’s filtering is primarily accomplished via the single, generic [`query`](https://shopify.dev/docs/api/admin-graphql/latest/queries/products#arguments-query) param.
Craft uses dedicated field names and types for argument inputs, so the above single- and multi-product queries accept any combination of criteria, including references to custom fields.

> [!TIP]
> Use the [GraphiQL IDE](https://craftcms.com/docs/5.x/development/graphql.html#using-the-graphiql-ide) in Craft’s control panel to explore the self-documenting API!

#### Extending

GraphQL is inherently strictly “typed,” which means that the arbitrary shape of Products’ `data` attribute is not selectable or navigable.

If you wish to expose [additional fields](#craftshopifyservicesapievent_define_product_gql_fields) you have synchronized from the API, they must be added as Craft builds the product element schema:

```php
use craft\base\Event;
use craft\events\DefineGqlTypeFieldsEvent;
use craft\gql\TypeManager;
use craft\shopify\elements\Product;
use GraphQL\Type\Definition\Type;

Event::on(
TypeManager::class,
TypeManager::EVENT_DEFINE_GQL_TYPE_FIELDS,
function(DefineGqlTypeFieldsEvent $event) {
// Exit early unless it’s the “type” definition we want to modify:
if ($event->typeName !== Product::GQL_TYPE_NAME) {
return;
}

// Register a new field that can resolve the supplemental `data`:
$event->fields['shopifyCategory'] = [
'name' => 'shopifyCategory',
'type' => Type::string(),
'description' => 'The Shopify “Standard Product Taxonomy” name.',
'resolve' => function(Product $source) {
return $source->getData()['category']['name'] ?? null;
},
];
}
)
```

As long as you keep your “resolver” in sync with your additional selections, you should be able to pass through those values.
The same strategy can be used to decorate other [built-in types](https://github.com/craftcms/shopify/tree/7.x/src/gql/types/) for images, metafields, options, and variants.
You can alter multiple types in a single `EVENT_DEFINE_GQL_TYPE_FIELDS` handler:

```php
if ($event->typeName === \craft\shopify\elements\Product::GQL_TYPE_NAME) {
// Manipulate product element fields...
}

if ($event->typeName === \craft\shopify\gql\types\Image::getName()) {
// Manipulate fields on "image" objects...
}
```

> [!WARNING]
> The fields we’ve added are not dynamically fetched from Shopify at runtime!
> This method just exposes additional fields that have already been synchronized from Shopify.



## Templating

### Product Data
Expand Down
54 changes: 53 additions & 1 deletion src/Plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
use craft\events\DefineConsoleActionsEvent;
use craft\events\DefineFieldLayoutFieldsEvent;
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;
Expand All @@ -30,6 +33,7 @@
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;
Expand All @@ -39,6 +43,8 @@
use craft\shopify\fieldlayoutelements\OptionsField;
use craft\shopify\fieldlayoutelements\VariantsField;
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;
Expand Down Expand Up @@ -73,7 +79,7 @@ class Plugin extends BasePlugin
/**
* @var string
*/
public string $schemaVersion = '7.0.0.1';
public string $schemaVersion = '7.1.0.0';

/**
* @inheritdoc
Expand Down Expand Up @@ -137,6 +143,9 @@ public function init()
$this->_registerResaveCommands();
$this->_registerGarbageCollection();
$this->_registerFeedMeEvents();
$this->_registerGqlInterfaces();
$this->_registerGqlQueries();
$this->_registerGqlComponents();

if (!$request->getIsConsoleRequest()) {
if ($request->getIsCpRequest()) {
Expand Down Expand Up @@ -251,6 +260,49 @@ private function _registerElementTypes(): void
});
}


/**
* Register the Gql interfaces
* @since 7.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 7.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 7.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
*
Expand Down
30 changes: 30 additions & 0 deletions src/elements/Product.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ class Product extends Element
public const SHOPIFY_STATUS_DRAFT = 'draft';
public const SHOPIFY_STATUS_ARCHIVED = 'archived';

/**
* @since 7.1.0
*/
public const GQL_TYPE_NAME = 'ShopifyProduct';

/**
* @return string|null
* @since 6.0.0
Expand Down Expand Up @@ -138,6 +143,12 @@ public function getDescriptionHtml(): ?string
*/
public ?string $templateSuffix = null;

/**
* @var int|null
* @since 7.1.0
*/
public ?int $totalInventory = null;

/**
* @var ?DateTime
*/
Expand Down Expand Up @@ -193,6 +204,25 @@ public function init(): void
parent::init();
}

/**
* @inheritdoc
* @since 7.1.0
*/
public static function gqlScopesByContext(mixed $context): array
{
/** @var FieldLayout $context */
return [self::GQL_TYPE_NAME];
}

/**
* @return string
* @since 7.1.0
*/
public function getGqlTypeName(): string
{
return self::GQL_TYPE_NAME;
}

/**
* @inheritdoc
*/
Expand Down
23 changes: 22 additions & 1 deletion src/elements/db/ProductQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ class ProductQuery extends ElementQuery
*/
public mixed $handle = null;


/**
* @var mixed|null
*/
Expand All @@ -63,6 +62,12 @@ class ProductQuery extends ElementQuery
*/
public mixed $templateSuffix = null;

/**
* @var mixed|null
* @since 7.1.0
*/
public mixed $totalInventory = null;

/**
* @var mixed|null
*/
Expand Down Expand Up @@ -258,6 +263,17 @@ public function templateSuffix(mixed $value): ProductQuery
return $this;
}

/**
* @param mixed $value
* @return ProductQuery
* @since 7.1.0
*/
public function totalInventory(mixed $value): ProductQuery
{
$this->totalInventory = $value;
return $this;
}

/**
* Narrows the query results based on the Shopify product ID
*/
Expand Down Expand Up @@ -396,6 +412,7 @@ protected function beforePrepare(): bool
'data.updatedAt',
'data.vendor',
'data.options',
'data.totalInventory',
'data.data',
]);

Expand Down Expand Up @@ -431,6 +448,10 @@ protected function beforePrepare(): bool
$this->subQuery->andWhere(Db::parseParam('data.templateSuffix', $this->templateSuffix));
}

if (isset($this->totalInventory)) {
$this->subQuery->andWhere(Db::parseParam('data.totalInventory', $this->totalInventory));
}

return parent::beforePrepare();
}
}
20 changes: 20 additions & 0 deletions src/fields/Products.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@

use Craft;
use craft\fields\BaseRelationField;
use craft\helpers\Gql;
use craft\shopify\elements\Product;
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 GraphQL\Type\Definition\Type;

/**
* Class Shopify Product Field
Expand Down Expand Up @@ -44,4 +49,19 @@ public static function elementType(): string
{
return Product::class;
}

/**
* @inheritdoc
* @since 7.1.0
*/
public function getContentGqlType(): Type|array
{
return [
'name' => $this->handle,
'type' => Type::listOf(ProductInterface::getType()),
'args' => ProductArguments::getArguments(),
'resolve' => ProductResolver::class . '::resolve',
'complexity' => Gql::eagerLoadComplexity(),
];
}
}
Loading
Loading