Browse Source

perf(core): Implement productVariantCount in collections query (#4132)

Will Nahmens 1 day ago
parent
commit
44bad119bc
24 changed files with 665 additions and 11 deletions
  1. 3 0
      packages/admin-ui/src/lib/core/src/common/generated-types.ts
  2. 3 0
      packages/asset-server-plugin/e2e/graphql/generated-e2e-asset-server-plugin-types.ts
  3. 3 0
      packages/common/src/generated-shop-types.ts
  4. 3 0
      packages/common/src/generated-types.ts
  5. 3 0
      packages/core/e2e/graphql/generated-e2e-admin-types.ts
  6. 3 0
      packages/core/e2e/graphql/generated-e2e-shop-types.ts
  7. 464 0
      packages/core/src/api/common/is-field-in-selection.spec.ts
  8. 75 0
      packages/core/src/api/common/is-field-in-selection.ts
  9. 16 2
      packages/core/src/api/resolvers/admin/collection.resolver.ts
  10. 19 1
      packages/core/src/api/resolvers/entity/collection-entity.resolver.ts
  11. 16 2
      packages/core/src/api/resolvers/shop/shop-products.resolver.ts
  12. 1 0
      packages/core/src/api/schema/common/collection.type.graphql
  13. 1 0
      packages/core/src/common/constants.ts
  14. 39 0
      packages/core/src/service/services/collection.service.ts
  15. 1 3
      packages/dashboard/src/app/routes/_authenticated/_collections/collections.graphql.ts
  16. 3 3
      packages/dashboard/src/app/routes/_authenticated/_collections/collections.tsx
  17. 0 0
      packages/dashboard/src/lib/graphql/graphql-env.d.ts
  18. 0 0
      packages/dev-server/graphql/graphql-env.d.ts
  19. 3 0
      packages/elasticsearch-plugin/e2e/graphql/generated-e2e-elasticsearch-plugin-types.ts
  20. 3 0
      packages/payments-plugin/e2e/graphql/generated-admin-types.ts
  21. 3 0
      packages/payments-plugin/e2e/graphql/generated-shop-types.ts
  22. 3 0
      packages/payments-plugin/src/mollie/graphql/generated-shop-types.ts
  23. 0 0
      schema-admin.json
  24. 0 0
      schema-shop.json

+ 3 - 0
packages/admin-ui/src/lib/core/src/common/generated-types.ts

@@ -490,6 +490,7 @@ export type Collection = Node & {
   parent?: Maybe<Collection>;
   parent?: Maybe<Collection>;
   parentId: Scalars['ID']['output'];
   parentId: Scalars['ID']['output'];
   position: Scalars['Int']['output'];
   position: Scalars['Int']['output'];
+  productVariantCount: Scalars['Int']['output'];
   productVariants: ProductVariantList;
   productVariants: ProductVariantList;
   slug: Scalars['String']['output'];
   slug: Scalars['String']['output'];
   translations: Array<CollectionTranslation>;
   translations: Array<CollectionTranslation>;
@@ -520,6 +521,7 @@ export type CollectionFilterParameter = {
   name?: InputMaybe<StringOperators>;
   name?: InputMaybe<StringOperators>;
   parentId?: InputMaybe<IdOperators>;
   parentId?: InputMaybe<IdOperators>;
   position?: InputMaybe<NumberOperators>;
   position?: InputMaybe<NumberOperators>;
+  productVariantCount?: InputMaybe<NumberOperators>;
   slug?: InputMaybe<StringOperators>;
   slug?: InputMaybe<StringOperators>;
   updatedAt?: InputMaybe<DateOperators>;
   updatedAt?: InputMaybe<DateOperators>;
 };
 };
@@ -561,6 +563,7 @@ export type CollectionSortParameter = {
   name?: InputMaybe<SortOrder>;
   name?: InputMaybe<SortOrder>;
   parentId?: InputMaybe<SortOrder>;
   parentId?: InputMaybe<SortOrder>;
   position?: InputMaybe<SortOrder>;
   position?: InputMaybe<SortOrder>;
+  productVariantCount?: InputMaybe<SortOrder>;
   slug?: InputMaybe<SortOrder>;
   slug?: InputMaybe<SortOrder>;
   updatedAt?: InputMaybe<SortOrder>;
   updatedAt?: InputMaybe<SortOrder>;
 };
 };

+ 3 - 0
packages/asset-server-plugin/e2e/graphql/generated-e2e-asset-server-plugin-types.ts

@@ -489,6 +489,7 @@ export type Collection = Node & {
     parent?: Maybe<Collection>;
     parent?: Maybe<Collection>;
     parentId: Scalars['ID']['output'];
     parentId: Scalars['ID']['output'];
     position: Scalars['Int']['output'];
     position: Scalars['Int']['output'];
+    productVariantCount: Scalars['Int']['output'];
     productVariants: ProductVariantList;
     productVariants: ProductVariantList;
     slug: Scalars['String']['output'];
     slug: Scalars['String']['output'];
     translations: Array<CollectionTranslation>;
     translations: Array<CollectionTranslation>;
@@ -517,6 +518,7 @@ export type CollectionFilterParameter = {
     name?: InputMaybe<StringOperators>;
     name?: InputMaybe<StringOperators>;
     parentId?: InputMaybe<IdOperators>;
     parentId?: InputMaybe<IdOperators>;
     position?: InputMaybe<NumberOperators>;
     position?: InputMaybe<NumberOperators>;
+    productVariantCount?: InputMaybe<NumberOperators>;
     slug?: InputMaybe<StringOperators>;
     slug?: InputMaybe<StringOperators>;
     updatedAt?: InputMaybe<DateOperators>;
     updatedAt?: InputMaybe<DateOperators>;
 };
 };
@@ -556,6 +558,7 @@ export type CollectionSortParameter = {
     name?: InputMaybe<SortOrder>;
     name?: InputMaybe<SortOrder>;
     parentId?: InputMaybe<SortOrder>;
     parentId?: InputMaybe<SortOrder>;
     position?: InputMaybe<SortOrder>;
     position?: InputMaybe<SortOrder>;
+    productVariantCount?: InputMaybe<SortOrder>;
     slug?: InputMaybe<SortOrder>;
     slug?: InputMaybe<SortOrder>;
     updatedAt?: InputMaybe<SortOrder>;
     updatedAt?: InputMaybe<SortOrder>;
 };
 };

+ 3 - 0
packages/common/src/generated-shop-types.ts

@@ -205,6 +205,7 @@ export type Collection = Node & {
     parent?: Maybe<Collection>;
     parent?: Maybe<Collection>;
     parentId: Scalars['ID']['output'];
     parentId: Scalars['ID']['output'];
     position: Scalars['Int']['output'];
     position: Scalars['Int']['output'];
+    productVariantCount: Scalars['Int']['output'];
     productVariants: ProductVariantList;
     productVariants: ProductVariantList;
     slug: Scalars['String']['output'];
     slug: Scalars['String']['output'];
     translations: Array<CollectionTranslation>;
     translations: Array<CollectionTranslation>;
@@ -232,6 +233,7 @@ export type CollectionFilterParameter = {
     name?: InputMaybe<StringOperators>;
     name?: InputMaybe<StringOperators>;
     parentId?: InputMaybe<IdOperators>;
     parentId?: InputMaybe<IdOperators>;
     position?: InputMaybe<NumberOperators>;
     position?: InputMaybe<NumberOperators>;
+    productVariantCount?: InputMaybe<NumberOperators>;
     slug?: InputMaybe<StringOperators>;
     slug?: InputMaybe<StringOperators>;
     updatedAt?: InputMaybe<DateOperators>;
     updatedAt?: InputMaybe<DateOperators>;
 };
 };
@@ -273,6 +275,7 @@ export type CollectionSortParameter = {
     name?: InputMaybe<SortOrder>;
     name?: InputMaybe<SortOrder>;
     parentId?: InputMaybe<SortOrder>;
     parentId?: InputMaybe<SortOrder>;
     position?: InputMaybe<SortOrder>;
     position?: InputMaybe<SortOrder>;
+    productVariantCount?: InputMaybe<SortOrder>;
     slug?: InputMaybe<SortOrder>;
     slug?: InputMaybe<SortOrder>;
     updatedAt?: InputMaybe<SortOrder>;
     updatedAt?: InputMaybe<SortOrder>;
 };
 };

+ 3 - 0
packages/common/src/generated-types.ts

@@ -485,6 +485,7 @@ export type Collection = Node & {
   parent?: Maybe<Collection>;
   parent?: Maybe<Collection>;
   parentId: Scalars['ID']['output'];
   parentId: Scalars['ID']['output'];
   position: Scalars['Int']['output'];
   position: Scalars['Int']['output'];
+  productVariantCount: Scalars['Int']['output'];
   productVariants: ProductVariantList;
   productVariants: ProductVariantList;
   slug: Scalars['String']['output'];
   slug: Scalars['String']['output'];
   translations: Array<CollectionTranslation>;
   translations: Array<CollectionTranslation>;
@@ -515,6 +516,7 @@ export type CollectionFilterParameter = {
   name?: InputMaybe<StringOperators>;
   name?: InputMaybe<StringOperators>;
   parentId?: InputMaybe<IdOperators>;
   parentId?: InputMaybe<IdOperators>;
   position?: InputMaybe<NumberOperators>;
   position?: InputMaybe<NumberOperators>;
+  productVariantCount?: InputMaybe<NumberOperators>;
   slug?: InputMaybe<StringOperators>;
   slug?: InputMaybe<StringOperators>;
   updatedAt?: InputMaybe<DateOperators>;
   updatedAt?: InputMaybe<DateOperators>;
 };
 };
@@ -556,6 +558,7 @@ export type CollectionSortParameter = {
   name?: InputMaybe<SortOrder>;
   name?: InputMaybe<SortOrder>;
   parentId?: InputMaybe<SortOrder>;
   parentId?: InputMaybe<SortOrder>;
   position?: InputMaybe<SortOrder>;
   position?: InputMaybe<SortOrder>;
+  productVariantCount?: InputMaybe<SortOrder>;
   slug?: InputMaybe<SortOrder>;
   slug?: InputMaybe<SortOrder>;
   updatedAt?: InputMaybe<SortOrder>;
   updatedAt?: InputMaybe<SortOrder>;
 };
 };

+ 3 - 0
packages/core/e2e/graphql/generated-e2e-admin-types.ts

@@ -489,6 +489,7 @@ export type Collection = Node & {
     parent?: Maybe<Collection>;
     parent?: Maybe<Collection>;
     parentId: Scalars['ID']['output'];
     parentId: Scalars['ID']['output'];
     position: Scalars['Int']['output'];
     position: Scalars['Int']['output'];
+    productVariantCount: Scalars['Int']['output'];
     productVariants: ProductVariantList;
     productVariants: ProductVariantList;
     slug: Scalars['String']['output'];
     slug: Scalars['String']['output'];
     translations: Array<CollectionTranslation>;
     translations: Array<CollectionTranslation>;
@@ -517,6 +518,7 @@ export type CollectionFilterParameter = {
     name?: InputMaybe<StringOperators>;
     name?: InputMaybe<StringOperators>;
     parentId?: InputMaybe<IdOperators>;
     parentId?: InputMaybe<IdOperators>;
     position?: InputMaybe<NumberOperators>;
     position?: InputMaybe<NumberOperators>;
+    productVariantCount?: InputMaybe<NumberOperators>;
     slug?: InputMaybe<StringOperators>;
     slug?: InputMaybe<StringOperators>;
     updatedAt?: InputMaybe<DateOperators>;
     updatedAt?: InputMaybe<DateOperators>;
 };
 };
@@ -556,6 +558,7 @@ export type CollectionSortParameter = {
     name?: InputMaybe<SortOrder>;
     name?: InputMaybe<SortOrder>;
     parentId?: InputMaybe<SortOrder>;
     parentId?: InputMaybe<SortOrder>;
     position?: InputMaybe<SortOrder>;
     position?: InputMaybe<SortOrder>;
+    productVariantCount?: InputMaybe<SortOrder>;
     slug?: InputMaybe<SortOrder>;
     slug?: InputMaybe<SortOrder>;
     updatedAt?: InputMaybe<SortOrder>;
     updatedAt?: InputMaybe<SortOrder>;
 };
 };

+ 3 - 0
packages/core/e2e/graphql/generated-e2e-shop-types.ts

@@ -196,6 +196,7 @@ export type Collection = Node & {
     parent?: Maybe<Collection>;
     parent?: Maybe<Collection>;
     parentId: Scalars['ID']['output'];
     parentId: Scalars['ID']['output'];
     position: Scalars['Int']['output'];
     position: Scalars['Int']['output'];
+    productVariantCount: Scalars['Int']['output'];
     productVariants: ProductVariantList;
     productVariants: ProductVariantList;
     slug: Scalars['String']['output'];
     slug: Scalars['String']['output'];
     translations: Array<CollectionTranslation>;
     translations: Array<CollectionTranslation>;
@@ -222,6 +223,7 @@ export type CollectionFilterParameter = {
     name?: InputMaybe<StringOperators>;
     name?: InputMaybe<StringOperators>;
     parentId?: InputMaybe<IdOperators>;
     parentId?: InputMaybe<IdOperators>;
     position?: InputMaybe<NumberOperators>;
     position?: InputMaybe<NumberOperators>;
+    productVariantCount?: InputMaybe<NumberOperators>;
     slug?: InputMaybe<StringOperators>;
     slug?: InputMaybe<StringOperators>;
     updatedAt?: InputMaybe<DateOperators>;
     updatedAt?: InputMaybe<DateOperators>;
 };
 };
@@ -261,6 +263,7 @@ export type CollectionSortParameter = {
     name?: InputMaybe<SortOrder>;
     name?: InputMaybe<SortOrder>;
     parentId?: InputMaybe<SortOrder>;
     parentId?: InputMaybe<SortOrder>;
     position?: InputMaybe<SortOrder>;
     position?: InputMaybe<SortOrder>;
+    productVariantCount?: InputMaybe<SortOrder>;
     slug?: InputMaybe<SortOrder>;
     slug?: InputMaybe<SortOrder>;
     updatedAt?: InputMaybe<SortOrder>;
     updatedAt?: InputMaybe<SortOrder>;
 };
 };

+ 464 - 0
packages/core/src/api/common/is-field-in-selection.spec.ts

@@ -0,0 +1,464 @@
+import { GraphQLResolveInfo, Kind, SelectionNode } from 'graphql';
+import { describe, expect, it } from 'vitest';
+
+import { isFieldInSelection } from './is-field-in-selection';
+
+/**
+ * Creates a mock GraphQLResolveInfo with the given selection structure.
+ * This simulates a query like:
+ * ```
+ * collections {
+ *   items {
+ *     id
+ *     name
+ *     productVariantCount
+ *   }
+ * }
+ * ```
+ */
+function createMockResolveInfo(
+    childSelections: SelectionNode[],
+    parentFieldName = 'items',
+    fragments: GraphQLResolveInfo['fragments'] = {},
+): GraphQLResolveInfo {
+    return {
+        fieldNodes: [
+            {
+                kind: Kind.FIELD,
+                name: { kind: Kind.NAME, value: 'collections' },
+                selectionSet: {
+                    kind: Kind.SELECTION_SET,
+                    selections: [
+                        {
+                            kind: Kind.FIELD,
+                            name: { kind: Kind.NAME, value: parentFieldName },
+                            selectionSet: {
+                                kind: Kind.SELECTION_SET,
+                                selections: childSelections,
+                            },
+                        },
+                    ],
+                },
+            },
+        ],
+        fragments,
+    } as unknown as GraphQLResolveInfo;
+}
+
+/**
+ * Helper to create field selections from field names
+ */
+function createFieldSelections(fieldNames: string[]): SelectionNode[] {
+    return fieldNames.map(field => ({
+        kind: Kind.FIELD as const,
+        name: { kind: Kind.NAME as const, value: field },
+    }));
+}
+
+describe('isFieldInSelection', () => {
+    describe('direct field selections', () => {
+        it('returns true when field is in selection', () => {
+            const info = createMockResolveInfo(createFieldSelections(['id', 'name', 'productVariantCount']));
+            expect(isFieldInSelection(info, 'productVariantCount')).toBe(true);
+        });
+
+        it('returns false when field is not in selection', () => {
+            const info = createMockResolveInfo(createFieldSelections(['id', 'name']));
+            expect(isFieldInSelection(info, 'productVariantCount')).toBe(false);
+        });
+
+        it('returns false when parent field does not exist', () => {
+            const info = createMockResolveInfo(createFieldSelections(['id', 'name']), 'nonexistent');
+            expect(isFieldInSelection(info, 'id', 'items')).toBe(false);
+        });
+
+        it('works with custom parent field name', () => {
+            const info = createMockResolveInfo(createFieldSelections(['id', 'name', 'slug']), 'children');
+            expect(isFieldInSelection(info, 'slug', 'children')).toBe(true);
+        });
+
+        it('returns false when selection set is empty', () => {
+            const info = createMockResolveInfo([]);
+            expect(isFieldInSelection(info, 'productVariantCount')).toBe(false);
+        });
+
+        it('is case-sensitive for field names', () => {
+            const info = createMockResolveInfo(createFieldSelections(['productVariantCount']));
+            expect(isFieldInSelection(info, 'ProductVariantCount')).toBe(false);
+            expect(isFieldInSelection(info, 'productvariantcount')).toBe(false);
+        });
+    });
+
+    describe('fragment spreads', () => {
+        it('returns true when field is in a fragment spread', () => {
+            // Simulates:
+            // collections {
+            //   items {
+            //     ...CollectionFields
+            //   }
+            // }
+            // fragment CollectionFields on Collection {
+            //   id
+            //   productVariantCount
+            // }
+            const fragments: GraphQLResolveInfo['fragments'] = {
+                CollectionFields: {
+                    kind: Kind.FRAGMENT_DEFINITION,
+                    name: { kind: Kind.NAME, value: 'CollectionFields' },
+                    typeCondition: {
+                        kind: Kind.NAMED_TYPE,
+                        name: { kind: Kind.NAME, value: 'Collection' },
+                    },
+                    selectionSet: {
+                        kind: Kind.SELECTION_SET,
+                        selections: createFieldSelections(['id', 'productVariantCount']),
+                    },
+                },
+            } as unknown as GraphQLResolveInfo['fragments'];
+
+            const selections: SelectionNode[] = [
+                {
+                    kind: Kind.FRAGMENT_SPREAD,
+                    name: { kind: Kind.NAME, value: 'CollectionFields' },
+                },
+            ];
+
+            const info = createMockResolveInfo(selections, 'items', fragments);
+            expect(isFieldInSelection(info, 'productVariantCount')).toBe(true);
+        });
+
+        it('returns false when field is not in a fragment spread', () => {
+            const fragments: GraphQLResolveInfo['fragments'] = {
+                CollectionFields: {
+                    kind: Kind.FRAGMENT_DEFINITION,
+                    name: { kind: Kind.NAME, value: 'CollectionFields' },
+                    typeCondition: {
+                        kind: Kind.NAMED_TYPE,
+                        name: { kind: Kind.NAME, value: 'Collection' },
+                    },
+                    selectionSet: {
+                        kind: Kind.SELECTION_SET,
+                        selections: createFieldSelections(['id', 'name']),
+                    },
+                },
+            } as unknown as GraphQLResolveInfo['fragments'];
+
+            const selections: SelectionNode[] = [
+                {
+                    kind: Kind.FRAGMENT_SPREAD,
+                    name: { kind: Kind.NAME, value: 'CollectionFields' },
+                },
+            ];
+
+            const info = createMockResolveInfo(selections, 'items', fragments);
+            expect(isFieldInSelection(info, 'productVariantCount')).toBe(false);
+        });
+
+        it('handles mixed direct fields and fragment spreads', () => {
+            // Simulates:
+            // collections {
+            //   items {
+            //     id
+            //     ...CollectionFields
+            //   }
+            // }
+            const fragments: GraphQLResolveInfo['fragments'] = {
+                CollectionFields: {
+                    kind: Kind.FRAGMENT_DEFINITION,
+                    name: { kind: Kind.NAME, value: 'CollectionFields' },
+                    typeCondition: {
+                        kind: Kind.NAMED_TYPE,
+                        name: { kind: Kind.NAME, value: 'Collection' },
+                    },
+                    selectionSet: {
+                        kind: Kind.SELECTION_SET,
+                        selections: createFieldSelections(['productVariantCount']),
+                    },
+                },
+            } as unknown as GraphQLResolveInfo['fragments'];
+
+            const selections: SelectionNode[] = [
+                ...createFieldSelections(['id', 'name']),
+                {
+                    kind: Kind.FRAGMENT_SPREAD,
+                    name: { kind: Kind.NAME, value: 'CollectionFields' },
+                },
+            ];
+
+            const info = createMockResolveInfo(selections, 'items', fragments);
+            expect(isFieldInSelection(info, 'id')).toBe(true);
+            expect(isFieldInSelection(info, 'productVariantCount')).toBe(true);
+        });
+
+        it('handles undefined fragment gracefully', () => {
+            const selections: SelectionNode[] = [
+                {
+                    kind: Kind.FRAGMENT_SPREAD,
+                    name: { kind: Kind.NAME, value: 'NonexistentFragment' },
+                },
+            ];
+
+            const info = createMockResolveInfo(selections, 'items', {});
+            expect(isFieldInSelection(info, 'productVariantCount')).toBe(false);
+        });
+
+        it('handles nested fragments', () => {
+            // Simulates:
+            // collections {
+            //   items {
+            //     ...OuterFragment
+            //   }
+            // }
+            // fragment OuterFragment on Collection {
+            //   id
+            //   ...InnerFragment
+            // }
+            // fragment InnerFragment on Collection {
+            //   productVariantCount
+            // }
+            const fragments: GraphQLResolveInfo['fragments'] = {
+                OuterFragment: {
+                    kind: Kind.FRAGMENT_DEFINITION,
+                    name: { kind: Kind.NAME, value: 'OuterFragment' },
+                    typeCondition: {
+                        kind: Kind.NAMED_TYPE,
+                        name: { kind: Kind.NAME, value: 'Collection' },
+                    },
+                    selectionSet: {
+                        kind: Kind.SELECTION_SET,
+                        selections: [
+                            ...createFieldSelections(['id']),
+                            {
+                                kind: Kind.FRAGMENT_SPREAD,
+                                name: { kind: Kind.NAME, value: 'InnerFragment' },
+                            },
+                        ],
+                    },
+                },
+                InnerFragment: {
+                    kind: Kind.FRAGMENT_DEFINITION,
+                    name: { kind: Kind.NAME, value: 'InnerFragment' },
+                    typeCondition: {
+                        kind: Kind.NAMED_TYPE,
+                        name: { kind: Kind.NAME, value: 'Collection' },
+                    },
+                    selectionSet: {
+                        kind: Kind.SELECTION_SET,
+                        selections: createFieldSelections(['productVariantCount']),
+                    },
+                },
+            } as unknown as GraphQLResolveInfo['fragments'];
+
+            const selections: SelectionNode[] = [
+                {
+                    kind: Kind.FRAGMENT_SPREAD,
+                    name: { kind: Kind.NAME, value: 'OuterFragment' },
+                },
+            ];
+
+            const info = createMockResolveInfo(selections, 'items', fragments);
+            expect(isFieldInSelection(info, 'productVariantCount')).toBe(true);
+            expect(isFieldInSelection(info, 'id')).toBe(true);
+        });
+    });
+
+    describe('inline fragments', () => {
+        it('returns true when field is in an inline fragment', () => {
+            // Simulates:
+            // collections {
+            //   items {
+            //     ... on Collection {
+            //       productVariantCount
+            //     }
+            //   }
+            // }
+            const selections: SelectionNode[] = [
+                {
+                    kind: Kind.INLINE_FRAGMENT,
+                    typeCondition: {
+                        kind: Kind.NAMED_TYPE,
+                        name: { kind: Kind.NAME, value: 'Collection' },
+                    },
+                    selectionSet: {
+                        kind: Kind.SELECTION_SET,
+                        selections: createFieldSelections(['productVariantCount']),
+                    },
+                },
+            ];
+
+            const info = createMockResolveInfo(selections);
+            expect(isFieldInSelection(info, 'productVariantCount')).toBe(true);
+        });
+
+        it('returns false when field is not in an inline fragment', () => {
+            const selections: SelectionNode[] = [
+                {
+                    kind: Kind.INLINE_FRAGMENT,
+                    typeCondition: {
+                        kind: Kind.NAMED_TYPE,
+                        name: { kind: Kind.NAME, value: 'Collection' },
+                    },
+                    selectionSet: {
+                        kind: Kind.SELECTION_SET,
+                        selections: createFieldSelections(['id', 'name']),
+                    },
+                },
+            ];
+
+            const info = createMockResolveInfo(selections);
+            expect(isFieldInSelection(info, 'productVariantCount')).toBe(false);
+        });
+
+        it('handles inline fragment without type condition', () => {
+            // Simulates:
+            // collections {
+            //   items {
+            //     ... {
+            //       productVariantCount
+            //     }
+            //   }
+            // }
+            const selections: SelectionNode[] = [
+                {
+                    kind: Kind.INLINE_FRAGMENT,
+                    selectionSet: {
+                        kind: Kind.SELECTION_SET,
+                        selections: createFieldSelections(['productVariantCount']),
+                    },
+                },
+            ];
+
+            const info = createMockResolveInfo(selections);
+            expect(isFieldInSelection(info, 'productVariantCount')).toBe(true);
+        });
+
+        it('handles mixed direct fields and inline fragments', () => {
+            const selections: SelectionNode[] = [
+                ...createFieldSelections(['id']),
+                {
+                    kind: Kind.INLINE_FRAGMENT,
+                    typeCondition: {
+                        kind: Kind.NAMED_TYPE,
+                        name: { kind: Kind.NAME, value: 'Collection' },
+                    },
+                    selectionSet: {
+                        kind: Kind.SELECTION_SET,
+                        selections: createFieldSelections(['productVariantCount']),
+                    },
+                },
+            ];
+
+            const info = createMockResolveInfo(selections);
+            expect(isFieldInSelection(info, 'id')).toBe(true);
+            expect(isFieldInSelection(info, 'productVariantCount')).toBe(true);
+        });
+    });
+
+    describe('parent field in fragments', () => {
+        it('finds parent field inside a fragment spread', () => {
+            // Simulates:
+            // collections {
+            //   ...PaginatedFields
+            // }
+            // fragment PaginatedFields on CollectionList {
+            //   items {
+            //     productVariantCount
+            //   }
+            // }
+            const fragments: GraphQLResolveInfo['fragments'] = {
+                PaginatedFields: {
+                    kind: Kind.FRAGMENT_DEFINITION,
+                    name: { kind: Kind.NAME, value: 'PaginatedFields' },
+                    typeCondition: {
+                        kind: Kind.NAMED_TYPE,
+                        name: { kind: Kind.NAME, value: 'CollectionList' },
+                    },
+                    selectionSet: {
+                        kind: Kind.SELECTION_SET,
+                        selections: [
+                            {
+                                kind: Kind.FIELD,
+                                name: { kind: Kind.NAME, value: 'items' },
+                                selectionSet: {
+                                    kind: Kind.SELECTION_SET,
+                                    selections: createFieldSelections(['productVariantCount']),
+                                },
+                            },
+                        ],
+                    },
+                },
+            } as unknown as GraphQLResolveInfo['fragments'];
+
+            const info = {
+                fieldNodes: [
+                    {
+                        kind: Kind.FIELD,
+                        name: { kind: Kind.NAME, value: 'collections' },
+                        selectionSet: {
+                            kind: Kind.SELECTION_SET,
+                            selections: [
+                                {
+                                    kind: Kind.FRAGMENT_SPREAD,
+                                    name: { kind: Kind.NAME, value: 'PaginatedFields' },
+                                },
+                            ],
+                        },
+                    },
+                ],
+                fragments,
+            } as unknown as GraphQLResolveInfo;
+
+            expect(isFieldInSelection(info, 'productVariantCount')).toBe(true);
+        });
+    });
+
+    describe('edge cases', () => {
+        it('handles missing selection set on parent field', () => {
+            const info = {
+                fieldNodes: [
+                    {
+                        kind: Kind.FIELD,
+                        name: { kind: Kind.NAME, value: 'collections' },
+                        selectionSet: {
+                            kind: Kind.SELECTION_SET,
+                            selections: [
+                                {
+                                    kind: Kind.FIELD,
+                                    name: { kind: Kind.NAME, value: 'items' },
+                                    // No selectionSet
+                                },
+                            ],
+                        },
+                    },
+                ],
+                fragments: {},
+            } as unknown as GraphQLResolveInfo;
+
+            expect(isFieldInSelection(info, 'productVariantCount')).toBe(false);
+        });
+
+        it('handles missing selection set on root field', () => {
+            const info = {
+                fieldNodes: [
+                    {
+                        kind: Kind.FIELD,
+                        name: { kind: Kind.NAME, value: 'collections' },
+                        // No selectionSet
+                    },
+                ],
+                fragments: {},
+            } as unknown as GraphQLResolveInfo;
+
+            expect(isFieldInSelection(info, 'productVariantCount')).toBe(false);
+        });
+
+        it('handles empty fieldNodes', () => {
+            const info = {
+                fieldNodes: [],
+                fragments: {},
+            } as unknown as GraphQLResolveInfo;
+
+            expect(isFieldInSelection(info, 'productVariantCount')).toBe(false);
+        });
+    });
+});

+ 75 - 0
packages/core/src/api/common/is-field-in-selection.ts

@@ -0,0 +1,75 @@
+import { FieldNode, GraphQLResolveInfo, SelectionNode } from 'graphql';
+
+/**
+ * Checks if a specific field is requested in the GraphQL query selection set.
+ * Looks for the field within the 'items' selection of a paginated list.
+ * Supports direct field selections, fragment spreads, and inline fragments.
+ */
+export function isFieldInSelection(
+    info: GraphQLResolveInfo,
+    fieldName: string,
+    parentFieldName = 'items',
+): boolean {
+    const parentSelections = info.fieldNodes.flatMap(node => node.selectionSet?.selections ?? []);
+    const parentField = findFieldInSelections(parentSelections, parentFieldName, info);
+    const childSelections = parentField?.selectionSet?.selections ?? [];
+    return hasFieldInSelections(childSelections, fieldName, info);
+}
+
+/**
+ * Finds a field by name in selections, including fragment spreads and inline fragments.
+ */
+function findFieldInSelections(
+    selections: readonly SelectionNode[],
+    fieldName: string,
+    info: GraphQLResolveInfo,
+): FieldNode | undefined {
+    for (const selection of selections) {
+        if (selection.kind === 'Field' && selection.name.value === fieldName) {
+            return selection;
+        }
+        if (selection.kind === 'FragmentSpread') {
+            const fragment = info.fragments[selection.name.value];
+            if (fragment) {
+                const found = findFieldInSelections(fragment.selectionSet.selections, fieldName, info);
+                if (found) {
+                    return found;
+                }
+            }
+        }
+        if (selection.kind === 'InlineFragment') {
+            const found = findFieldInSelections(selection.selectionSet.selections, fieldName, info);
+            if (found) {
+                return found;
+            }
+        }
+    }
+    return undefined;
+}
+
+/**
+ * Checks if a field exists in selections, including fragment spreads and inline fragments.
+ */
+function hasFieldInSelections(
+    selections: readonly SelectionNode[],
+    fieldName: string,
+    info: GraphQLResolveInfo,
+): boolean {
+    for (const selection of selections) {
+        if (selection.kind === 'Field' && selection.name.value === fieldName) {
+            return true;
+        }
+        if (selection.kind === 'FragmentSpread') {
+            const fragment = info.fragments[selection.name.value];
+            if (fragment && hasFieldInSelections(fragment.selectionSet.selections, fieldName, info)) {
+                return true;
+            }
+        }
+        if (selection.kind === 'InlineFragment') {
+            if (hasFieldInSelections(selection.selectionSet.selections, fieldName, info)) {
+                return true;
+            }
+        }
+    }
+    return false;
+}

+ 16 - 2
packages/core/src/api/resolvers/admin/collection.resolver.ts

@@ -1,4 +1,4 @@
-import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
+import { Args, Info, Mutation, Query, Resolver } from '@nestjs/graphql';
 import {
 import {
     ConfigurableOperationDefinition,
     ConfigurableOperationDefinition,
     DeletionResponse,
     DeletionResponse,
@@ -15,7 +15,10 @@ import {
     QueryPreviewCollectionVariantsArgs,
     QueryPreviewCollectionVariantsArgs,
 } from '@vendure/common/lib/generated-types';
 } from '@vendure/common/lib/generated-types';
 import { PaginatedList } from '@vendure/common/lib/shared-types';
 import { PaginatedList } from '@vendure/common/lib/shared-types';
+import { GraphQLResolveInfo } from 'graphql';
 
 
+import { RequestContextCacheService } from '../../../cache/request-context-cache.service';
+import { CacheKey } from '../../../common/constants';
 import { UserInputError } from '../../../common/error/errors';
 import { UserInputError } from '../../../common/error/errors';
 import { Translated } from '../../../common/types/locale-types';
 import { Translated } from '../../../common/types/locale-types';
 import { CollectionFilter } from '../../../config/catalog/collection-filter';
 import { CollectionFilter } from '../../../config/catalog/collection-filter';
@@ -23,6 +26,7 @@ import { Collection } from '../../../entity/collection/collection.entity';
 import { CollectionService } from '../../../service/services/collection.service';
 import { CollectionService } from '../../../service/services/collection.service';
 import { FacetValueService } from '../../../service/services/facet-value.service';
 import { FacetValueService } from '../../../service/services/facet-value.service';
 import { ConfigurableOperationCodec } from '../../common/configurable-operation-codec';
 import { ConfigurableOperationCodec } from '../../common/configurable-operation-codec';
+import { isFieldInSelection } from '../../common/is-field-in-selection';
 import { RequestContext } from '../../common/request-context';
 import { RequestContext } from '../../common/request-context';
 import { Allow } from '../../decorators/allow.decorator';
 import { Allow } from '../../decorators/allow.decorator';
 import { RelationPaths, Relations } from '../../decorators/relations.decorator';
 import { RelationPaths, Relations } from '../../decorators/relations.decorator';
@@ -35,6 +39,7 @@ export class CollectionResolver {
         private collectionService: CollectionService,
         private collectionService: CollectionService,
         private facetValueService: FacetValueService,
         private facetValueService: FacetValueService,
         private configurableOperationCodec: ConfigurableOperationCodec,
         private configurableOperationCodec: ConfigurableOperationCodec,
+        private requestContextCache: RequestContextCacheService,
     ) {}
     ) {}
 
 
     @Query()
     @Query()
@@ -56,8 +61,17 @@ export class CollectionResolver {
             omit: ['productVariants', 'assets', 'parent.productVariants', 'children.productVariants'],
             omit: ['productVariants', 'assets', 'parent.productVariants', 'children.productVariants'],
         })
         })
         relations: RelationPaths<Collection>,
         relations: RelationPaths<Collection>,
+        @Info() info: GraphQLResolveInfo,
     ): Promise<PaginatedList<Translated<Collection>>> {
     ): Promise<PaginatedList<Translated<Collection>>> {
-        return this.collectionService.findAll(ctx, args.options || undefined, relations);
+        const collections = await this.collectionService.findAll(ctx, args.options || undefined, relations);
+        // Cache the variant counts query promise if productVariantCount is requested,
+        // allowing the DB query to start before the field resolvers are called
+        if (isFieldInSelection(info, 'productVariantCount')) {
+            const collectionIds = collections.items.map(c => c.id);
+            const countsPromise = this.collectionService.getProductVariantCounts(ctx, collectionIds);
+            this.requestContextCache.set(ctx, CacheKey.CollectionVariantCounts, countsPromise);
+        }
+        return collections;
     }
     }
 
 
     @Query()
     @Query()

+ 19 - 1
packages/core/src/api/resolvers/entity/collection-entity.resolver.ts

@@ -5,8 +5,10 @@ import {
     ConfigurableOperation,
     ConfigurableOperation,
     ProductVariantListOptions,
     ProductVariantListOptions,
 } from '@vendure/common/lib/generated-types';
 } from '@vendure/common/lib/generated-types';
-import { PaginatedList } from '@vendure/common/lib/shared-types';
+import { ID, PaginatedList } from '@vendure/common/lib/shared-types';
 
 
+import { RequestContextCacheService } from '../../../cache/request-context-cache.service';
+import { CacheKey } from '../../../common/constants';
 import { ListQueryOptions } from '../../../common/types/common-types';
 import { ListQueryOptions } from '../../../common/types/common-types';
 import { Translated } from '../../../common/types/locale-types';
 import { Translated } from '../../../common/types/locale-types';
 import { CollectionFilter } from '../../../config/catalog/collection-filter';
 import { CollectionFilter } from '../../../config/catalog/collection-filter';
@@ -30,6 +32,7 @@ export class CollectionEntityResolver {
         private assetService: AssetService,
         private assetService: AssetService,
         private localeStringHydrator: LocaleStringHydrator,
         private localeStringHydrator: LocaleStringHydrator,
         private configurableOperationCodec: ConfigurableOperationCodec,
         private configurableOperationCodec: ConfigurableOperationCodec,
+        private requestContextCache: RequestContextCacheService,
     ) {}
     ) {}
 
 
     @ResolveField()
     @ResolveField()
@@ -73,6 +76,21 @@ export class CollectionEntityResolver {
         return this.productVariantService.getVariantsByCollectionId(ctx, collection.id, options, relations);
         return this.productVariantService.getVariantsByCollectionId(ctx, collection.id, options, relations);
     }
     }
 
 
+    @ResolveField()
+    async productVariantCount(@Ctx() ctx: RequestContext, @Parent() collection: Collection): Promise<number> {
+        const cachedCountsPromise = this.requestContextCache.get<Promise<Map<ID, number>>>(
+            ctx,
+            CacheKey.CollectionVariantCounts,
+        );
+        if (cachedCountsPromise) {
+            const countsMap = await cachedCountsPromise;
+            return countsMap.get(String(collection.id)) ?? 0;
+        }
+        // Fallback to single query if cache not available (e.g., single collection query)
+        const singleCountMap = await this.collectionService.getProductVariantCounts(ctx, [collection.id]);
+        return singleCountMap.get(String(collection.id)) ?? 0;
+    }
+
     @ResolveField()
     @ResolveField()
     async breadcrumbs(
     async breadcrumbs(
         @Ctx() ctx: RequestContext,
         @Ctx() ctx: RequestContext,

+ 16 - 2
packages/core/src/api/resolvers/shop/shop-products.resolver.ts

@@ -1,4 +1,4 @@
-import { Args, Query, Resolver } from '@nestjs/graphql';
+import { Args, Info, Query, Resolver } from '@nestjs/graphql';
 import {
 import {
     QueryCollectionArgs,
     QueryCollectionArgs,
     QueryCollectionsArgs,
     QueryCollectionsArgs,
@@ -10,7 +10,10 @@ import {
 } from '@vendure/common/lib/generated-shop-types';
 } from '@vendure/common/lib/generated-shop-types';
 import { Omit } from '@vendure/common/lib/omit';
 import { Omit } from '@vendure/common/lib/omit';
 import { PaginatedList } from '@vendure/common/lib/shared-types';
 import { PaginatedList } from '@vendure/common/lib/shared-types';
+import { GraphQLResolveInfo } from 'graphql';
 
 
+import { RequestContextCacheService } from '../../../cache/request-context-cache.service';
+import { CacheKey } from '../../../common/constants';
 import { InternalServerError, UserInputError } from '../../../common/error/errors';
 import { InternalServerError, UserInputError } from '../../../common/error/errors';
 import { ListQueryOptions } from '../../../common/types/common-types';
 import { ListQueryOptions } from '../../../common/types/common-types';
 import { Translated } from '../../../common/types/locale-types';
 import { Translated } from '../../../common/types/locale-types';
@@ -21,6 +24,7 @@ import { CollectionService, FacetService } from '../../../service';
 import { FacetValueService } from '../../../service/services/facet-value.service';
 import { FacetValueService } from '../../../service/services/facet-value.service';
 import { ProductVariantService } from '../../../service/services/product-variant.service';
 import { ProductVariantService } from '../../../service/services/product-variant.service';
 import { ProductService } from '../../../service/services/product.service';
 import { ProductService } from '../../../service/services/product.service';
+import { isFieldInSelection } from '../../common/is-field-in-selection';
 import { RequestContext } from '../../common/request-context';
 import { RequestContext } from '../../common/request-context';
 import { RelationPaths, Relations } from '../../decorators/relations.decorator';
 import { RelationPaths, Relations } from '../../decorators/relations.decorator';
 import { Ctx } from '../../decorators/request-context.decorator';
 import { Ctx } from '../../decorators/request-context.decorator';
@@ -33,6 +37,7 @@ export class ShopProductsResolver {
         private facetValueService: FacetValueService,
         private facetValueService: FacetValueService,
         private collectionService: CollectionService,
         private collectionService: CollectionService,
         private facetService: FacetService,
         private facetService: FacetService,
+        private requestContextCache: RequestContextCacheService,
     ) {}
     ) {}
 
 
     @Query()
     @Query()
@@ -84,6 +89,7 @@ export class ShopProductsResolver {
             omit: ['productVariants', 'assets', 'parent.productVariants', 'children.productVariants'],
             omit: ['productVariants', 'assets', 'parent.productVariants', 'children.productVariants'],
         })
         })
         relations: RelationPaths<Collection>,
         relations: RelationPaths<Collection>,
+        @Info() info: GraphQLResolveInfo,
     ): Promise<PaginatedList<Translated<Collection>>> {
     ): Promise<PaginatedList<Translated<Collection>>> {
         const options: ListQueryOptions<Collection> = {
         const options: ListQueryOptions<Collection> = {
             ...args.options,
             ...args.options,
@@ -92,7 +98,15 @@ export class ShopProductsResolver {
                 isPrivate: { eq: false },
                 isPrivate: { eq: false },
             },
             },
         };
         };
-        return this.collectionService.findAll(ctx, options || undefined, relations);
+        const collections = await this.collectionService.findAll(ctx, options || undefined, relations);
+        // Cache the variant counts query promise if productVariantCount is requested,
+        // allowing the DB query to start before the field resolvers are called
+        if (isFieldInSelection(info, 'productVariantCount')) {
+            const collectionIds = collections.items.map(c => c.id);
+            const countsPromise = this.collectionService.getProductVariantCounts(ctx, collectionIds);
+            this.requestContextCache.set(ctx, CacheKey.CollectionVariantCounts, countsPromise);
+        }
+        return collections;
     }
     }
 
 
     @Query()
     @Query()

+ 1 - 0
packages/core/src/api/schema/common/collection.type.graphql

@@ -16,6 +16,7 @@ type Collection implements Node {
     filters: [ConfigurableOperation!]!
     filters: [ConfigurableOperation!]!
     translations: [CollectionTranslation!]!
     translations: [CollectionTranslation!]!
     productVariants(options: ProductVariantListOptions): ProductVariantList!
     productVariants(options: ProductVariantListOptions): ProductVariantList!
+    productVariantCount: Int!
 }
 }
 
 
 type CollectionBreadcrumb {
 type CollectionBreadcrumb {

+ 1 - 0
packages/core/src/common/constants.ts

@@ -82,4 +82,5 @@ export const CacheKey = {
     AllZones: 'AllZones',
     AllZones: 'AllZones',
     ActiveTaxZone: 'ActiveTaxZone',
     ActiveTaxZone: 'ActiveTaxZone',
     ActiveTaxZone_PPA: 'ActiveTaxZone_PPA',
     ActiveTaxZone_PPA: 'ActiveTaxZone_PPA',
+    CollectionVariantCounts: 'CollectionService.getProductVariantCounts',
 };
 };

+ 39 - 0
packages/core/src/service/services/collection.service.ts

@@ -317,6 +317,45 @@ export class CollectionService implements OnModuleInit {
         return this.getDescendants(ctx, collectionId, 1);
         return this.getDescendants(ctx, collectionId, 1);
     }
     }
 
 
+    /**
+     * @description
+     * Returns a Map of collection IDs to their product variant counts.
+     * This performs a single bulk query to get counts for all provided collection IDs,
+     * avoiding N+1 query issues when resolving productVariantCount on multiple collections.
+     */
+    async getProductVariantCounts(ctx: RequestContext, collectionIds: ID[]): Promise<Map<ID, number>> {
+        if (collectionIds.length === 0) {
+            return new Map();
+        }
+        const results = await this.connection
+            .getRepository(ctx, ProductVariant)
+            .createQueryBuilder('productvariant')
+            .select('collection.id', 'collectionId')
+            .addSelect('COUNT(DISTINCT productvariant.id)', 'count')
+            .innerJoin('productvariant.channels', 'channel', 'channel.id = :channelId', {
+                channelId: ctx.channelId,
+            })
+            .innerJoin('productvariant.collections', 'collection', 'collection.id IN (:...collectionIds)', {
+                collectionIds,
+            })
+            .innerJoin('productvariant.product', 'product')
+            .andWhere('product.deletedAt IS NULL')
+            .andWhere('productvariant.deletedAt IS NULL')
+            .groupBy('collection.id')
+            .getRawMany<{ collectionId: string; count: string }>();
+
+        const countMap = new Map<ID, number>();
+        // Normalize IDs to strings to ensure consistent Map key types,
+        // since raw query results return collectionId as string
+        for (const id of collectionIds) {
+            countMap.set(String(id), 0);
+        }
+        for (const result of results) {
+            countMap.set(String(result.collectionId), Number(result.count));
+        }
+        return countMap;
+    }
+
     /**
     /**
      * @description
      * @description
      * Returns an array of name/id pairs representing all ancestor Collections up
      * Returns an array of name/id pairs representing all ancestor Collections up

+ 1 - 3
packages/dashboard/src/app/routes/_authenticated/_collections/collections.graphql.ts

@@ -33,9 +33,7 @@ export const collectionListDocument = graphql(
                     position
                     position
                     isPrivate
                     isPrivate
                     parentId
                     parentId
-                    productVariants {
-                        totalItems
-                    }
+                    productVariantCount
                 }
                 }
                 totalItems
                 totalItems
             }
             }

+ 3 - 3
packages/dashboard/src/app/routes/_authenticated/_collections/collections.tsx

@@ -218,7 +218,7 @@ function CollectionListPage() {
                         );
                         );
                     },
                     },
                 },
                 },
-                productVariants: {
+                productVariantCount: {
                     header: () => <Trans>Contents</Trans>,
                     header: () => <Trans>Contents</Trans>,
                     cell: ({ row }) => {
                     cell: ({ row }) => {
                         return (
                         return (
@@ -226,7 +226,7 @@ function CollectionListPage() {
                                 collectionId={row.original.id}
                                 collectionId={row.original.id}
                                 collectionName={row.original.name}
                                 collectionName={row.original.name}
                             >
                             >
-                                <Trans>{row.original.productVariants?.totalItems} variants</Trans>
+                                <Trans>{row.original.productVariantCount} variants</Trans>
                             </CollectionContentsSheet>
                             </CollectionContentsSheet>
                         );
                         );
                     },
                     },
@@ -257,7 +257,7 @@ function CollectionListPage() {
                 'name',
                 'name',
                 'slug',
                 'slug',
                 'breadcrumbs',
                 'breadcrumbs',
-                'productVariants',
+                'productVariantCount',
             ]}
             ]}
             transformData={data => {
             transformData={data => {
                 return addSubCollections(data);
                 return addSubCollections(data);

File diff suppressed because it is too large
+ 0 - 0
packages/dashboard/src/lib/graphql/graphql-env.d.ts


File diff suppressed because it is too large
+ 0 - 0
packages/dev-server/graphql/graphql-env.d.ts


+ 3 - 0
packages/elasticsearch-plugin/e2e/graphql/generated-e2e-elasticsearch-plugin-types.ts

@@ -489,6 +489,7 @@ export type Collection = Node & {
     parent?: Maybe<Collection>;
     parent?: Maybe<Collection>;
     parentId: Scalars['ID']['output'];
     parentId: Scalars['ID']['output'];
     position: Scalars['Int']['output'];
     position: Scalars['Int']['output'];
+    productVariantCount: Scalars['Int']['output'];
     productVariants: ProductVariantList;
     productVariants: ProductVariantList;
     slug: Scalars['String']['output'];
     slug: Scalars['String']['output'];
     translations: Array<CollectionTranslation>;
     translations: Array<CollectionTranslation>;
@@ -517,6 +518,7 @@ export type CollectionFilterParameter = {
     name?: InputMaybe<StringOperators>;
     name?: InputMaybe<StringOperators>;
     parentId?: InputMaybe<IdOperators>;
     parentId?: InputMaybe<IdOperators>;
     position?: InputMaybe<NumberOperators>;
     position?: InputMaybe<NumberOperators>;
+    productVariantCount?: InputMaybe<NumberOperators>;
     slug?: InputMaybe<StringOperators>;
     slug?: InputMaybe<StringOperators>;
     updatedAt?: InputMaybe<DateOperators>;
     updatedAt?: InputMaybe<DateOperators>;
 };
 };
@@ -556,6 +558,7 @@ export type CollectionSortParameter = {
     name?: InputMaybe<SortOrder>;
     name?: InputMaybe<SortOrder>;
     parentId?: InputMaybe<SortOrder>;
     parentId?: InputMaybe<SortOrder>;
     position?: InputMaybe<SortOrder>;
     position?: InputMaybe<SortOrder>;
+    productVariantCount?: InputMaybe<SortOrder>;
     slug?: InputMaybe<SortOrder>;
     slug?: InputMaybe<SortOrder>;
     updatedAt?: InputMaybe<SortOrder>;
     updatedAt?: InputMaybe<SortOrder>;
 };
 };

+ 3 - 0
packages/payments-plugin/e2e/graphql/generated-admin-types.ts

@@ -493,6 +493,7 @@ export type Collection = Node & {
     parent?: Maybe<Collection>;
     parent?: Maybe<Collection>;
     parentId: Scalars['ID']['output'];
     parentId: Scalars['ID']['output'];
     position: Scalars['Int']['output'];
     position: Scalars['Int']['output'];
+    productVariantCount: Scalars['Int']['output'];
     productVariants: ProductVariantList;
     productVariants: ProductVariantList;
     slug: Scalars['String']['output'];
     slug: Scalars['String']['output'];
     translations: Array<CollectionTranslation>;
     translations: Array<CollectionTranslation>;
@@ -521,6 +522,7 @@ export type CollectionFilterParameter = {
     name?: InputMaybe<StringOperators>;
     name?: InputMaybe<StringOperators>;
     parentId?: InputMaybe<IdOperators>;
     parentId?: InputMaybe<IdOperators>;
     position?: InputMaybe<NumberOperators>;
     position?: InputMaybe<NumberOperators>;
+    productVariantCount?: InputMaybe<NumberOperators>;
     slug?: InputMaybe<StringOperators>;
     slug?: InputMaybe<StringOperators>;
     updatedAt?: InputMaybe<DateOperators>;
     updatedAt?: InputMaybe<DateOperators>;
 };
 };
@@ -560,6 +562,7 @@ export type CollectionSortParameter = {
     name?: InputMaybe<SortOrder>;
     name?: InputMaybe<SortOrder>;
     parentId?: InputMaybe<SortOrder>;
     parentId?: InputMaybe<SortOrder>;
     position?: InputMaybe<SortOrder>;
     position?: InputMaybe<SortOrder>;
+    productVariantCount?: InputMaybe<SortOrder>;
     slug?: InputMaybe<SortOrder>;
     slug?: InputMaybe<SortOrder>;
     updatedAt?: InputMaybe<SortOrder>;
     updatedAt?: InputMaybe<SortOrder>;
 };
 };

+ 3 - 0
packages/payments-plugin/e2e/graphql/generated-shop-types.ts

@@ -200,6 +200,7 @@ export type Collection = Node & {
     parent?: Maybe<Collection>;
     parent?: Maybe<Collection>;
     parentId: Scalars['ID']['output'];
     parentId: Scalars['ID']['output'];
     position: Scalars['Int']['output'];
     position: Scalars['Int']['output'];
+    productVariantCount: Scalars['Int']['output'];
     productVariants: ProductVariantList;
     productVariants: ProductVariantList;
     slug: Scalars['String']['output'];
     slug: Scalars['String']['output'];
     translations: Array<CollectionTranslation>;
     translations: Array<CollectionTranslation>;
@@ -226,6 +227,7 @@ export type CollectionFilterParameter = {
     name?: InputMaybe<StringOperators>;
     name?: InputMaybe<StringOperators>;
     parentId?: InputMaybe<IdOperators>;
     parentId?: InputMaybe<IdOperators>;
     position?: InputMaybe<NumberOperators>;
     position?: InputMaybe<NumberOperators>;
+    productVariantCount?: InputMaybe<NumberOperators>;
     slug?: InputMaybe<StringOperators>;
     slug?: InputMaybe<StringOperators>;
     updatedAt?: InputMaybe<DateOperators>;
     updatedAt?: InputMaybe<DateOperators>;
 };
 };
@@ -265,6 +267,7 @@ export type CollectionSortParameter = {
     name?: InputMaybe<SortOrder>;
     name?: InputMaybe<SortOrder>;
     parentId?: InputMaybe<SortOrder>;
     parentId?: InputMaybe<SortOrder>;
     position?: InputMaybe<SortOrder>;
     position?: InputMaybe<SortOrder>;
+    productVariantCount?: InputMaybe<SortOrder>;
     slug?: InputMaybe<SortOrder>;
     slug?: InputMaybe<SortOrder>;
     updatedAt?: InputMaybe<SortOrder>;
     updatedAt?: InputMaybe<SortOrder>;
 };
 };

+ 3 - 0
packages/payments-plugin/src/mollie/graphql/generated-shop-types.ts

@@ -209,6 +209,7 @@ export type Collection = Node & {
     parent?: Maybe<Collection>;
     parent?: Maybe<Collection>;
     parentId: Scalars['ID']['output'];
     parentId: Scalars['ID']['output'];
     position: Scalars['Int']['output'];
     position: Scalars['Int']['output'];
+    productVariantCount: Scalars['Int']['output'];
     productVariants: ProductVariantList;
     productVariants: ProductVariantList;
     slug: Scalars['String']['output'];
     slug: Scalars['String']['output'];
     translations: Array<CollectionTranslation>;
     translations: Array<CollectionTranslation>;
@@ -236,6 +237,7 @@ export type CollectionFilterParameter = {
     name?: InputMaybe<StringOperators>;
     name?: InputMaybe<StringOperators>;
     parentId?: InputMaybe<IdOperators>;
     parentId?: InputMaybe<IdOperators>;
     position?: InputMaybe<NumberOperators>;
     position?: InputMaybe<NumberOperators>;
+    productVariantCount?: InputMaybe<NumberOperators>;
     slug?: InputMaybe<StringOperators>;
     slug?: InputMaybe<StringOperators>;
     updatedAt?: InputMaybe<DateOperators>;
     updatedAt?: InputMaybe<DateOperators>;
 };
 };
@@ -277,6 +279,7 @@ export type CollectionSortParameter = {
     name?: InputMaybe<SortOrder>;
     name?: InputMaybe<SortOrder>;
     parentId?: InputMaybe<SortOrder>;
     parentId?: InputMaybe<SortOrder>;
     position?: InputMaybe<SortOrder>;
     position?: InputMaybe<SortOrder>;
+    productVariantCount?: InputMaybe<SortOrder>;
     slug?: InputMaybe<SortOrder>;
     slug?: InputMaybe<SortOrder>;
     updatedAt?: InputMaybe<SortOrder>;
     updatedAt?: InputMaybe<SortOrder>;
 };
 };

File diff suppressed because it is too large
+ 0 - 0
schema-admin.json


File diff suppressed because it is too large
+ 0 - 0
schema-shop.json


Some files were not shown because too many files changed in this diff