Jelajahi Sumber

perf(dashboard): Optimize list queries (#3838)

Michael Bromley 3 bulan lalu
induk
melakukan
8709fda964

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

@@ -96,13 +96,18 @@ function CollectionListPage() {
                 }}
                 customizeColumns={{
                     name: {
+                        meta: {
+                            // This column needs the following fields to always be available
+                            // in order to correctly render.
+                            dependencies: ['children', 'breadcrumbs'],
+                        },
                         header: 'Collection Name',
                         cell: ({ row }) => {
                             const isExpanded = row.getIsExpanded();
                             const hasChildren = !!row.original.children?.length;
                             return (
                                 <div
-                                    style={{ marginLeft: (row.original.breadcrumbs.length - 2) * 20 + 'px' }}
+                                    style={{ marginLeft: (row.original.breadcrumbs?.length - 2) * 20 + 'px' }}
                                     className="flex gap-2 items-center"
                                 >
                                     <Button
@@ -143,7 +148,7 @@ function CollectionListPage() {
                                     collectionId={row.original.id}
                                     collectionName={row.original.name}
                                 >
-                                    <Trans>{row.original.productVariants.totalItems} variants</Trans>
+                                    <Trans>{row.original.productVariants?.totalItems} variants</Trans>
                                 </CollectionContentsSheet>
                             );
                         },

+ 2 - 0
packages/dashboard/src/lib/components/data-table/use-generated-columns.tsx

@@ -196,6 +196,7 @@ export function useGeneratedColumns<T extends TypedDocumentNode<any, any>>({
                     />
                 ),
                 enableColumnFilter: false,
+                enableHiding: false,
                 cell: ({ row }) => {
                     return (
                         <Checkbox
@@ -224,6 +225,7 @@ function getRowActions(
         accessorKey: 'actions',
         header: () => <Trans>Actions</Trans>,
         enableColumnFilter: false,
+        enableHiding: false,
         cell: ({ row, table }) => {
             return (
                 <DropdownMenu>

+ 59 - 32
packages/dashboard/src/lib/components/shared/paginated-list-data-table.tsx

@@ -1,13 +1,15 @@
-import { DataTable, FacetedFilter } from '\@/vdb/components/data-table/data-table.js';
-import { getObjectPathToPaginatedList } from '\@/vdb/framework/document-introspection/get-document-structure.js';
-import { useListQueryFields } from '\@/vdb/framework/document-introspection/hooks.js';
-import { api } from '\@/vdb/graphql/api.js';
+import { DataTable, FacetedFilter } from '@/vdb/components/data-table/data-table.js';
+import { getObjectPathToPaginatedList } from '@/vdb/framework/document-introspection/get-document-structure.js';
+import { useListQueryFields } from '@/vdb/framework/document-introspection/hooks.js';
+import { api } from '@/vdb/graphql/api.js';
 import { keepPreviousData, useQuery, useQueryClient } from '@tanstack/react-query';
 import { useDebounce } from '@uidotdev/usehooks';
 
-import { BulkAction } from '\@/vdb/framework/extension-api/types/index.js';
-import { ResultOf } from '\@/vdb/graphql/graphql.js';
-import { useExtendedListQuery } from '\@/vdb/hooks/use-extended-list-query.js';
+import { addCustomFields } from '@/vdb/framework/document-introspection/add-custom-fields.js';
+import { includeOnlySelectedListFields } from '@/vdb/framework/document-introspection/include-only-selected-list-fields.js';
+import { BulkAction } from '@/vdb/framework/extension-api/types/index.js';
+import { ResultOf } from '@/vdb/graphql/graphql.js';
+import { useExtendedListQuery } from '@/vdb/hooks/use-extended-list-query.js';
 import { TypedDocumentNode } from '@graphql-typed-document-node/core';
 import { ColumnFiltersState, ColumnSort, SortingState, Table } from '@tanstack/react-table';
 import { ColumnDef, Row, TableOptions, VisibilityState } from '@tanstack/table-core';
@@ -88,7 +90,18 @@ export type AllItemFieldKeys<T extends TypedDocumentNode<any, any>> =
     | CustomFieldKeysOfItem<PaginatedListItemFields<T>>;
 
 export type CustomizeColumnConfig<T extends TypedDocumentNode<any, any>> = {
-    [Key in AllItemFieldKeys<T>]?: Partial<ColumnDef<PaginatedListItemFields<T>, any>>;
+    [Key in AllItemFieldKeys<T>]?: Partial<ColumnDef<PaginatedListItemFields<T>, any>> & {
+        meta?: {
+            /**
+             * @description
+             * Columns that rely on _other_ columns in order to correctly render,
+             * can declare those other columns as dependencies in order to ensure that
+             * those columns are always fetched, even when those columns are not explicitly
+             * included in the visible table columns.
+             */
+            dependencies?: Array<AllItemFieldKeys<T>>;
+        };
+    };
 };
 
 export type FacetedFilterConfig<T extends TypedDocumentNode<any, any>> = {
@@ -365,7 +378,7 @@ export function PaginatedListDataTable<
     const [searchTerm, setSearchTerm] = React.useState<string>('');
     const debouncedSearchTerm = useDebounce(searchTerm, 500);
     const queryClient = useQueryClient();
-    const extendedListQuery = useExtendedListQuery(listQuery);
+    const extendedListQuery = useExtendedListQuery(addCustomFields(listQuery));
 
     const sort = sorting?.reduce((acc: any, sort: ColumnSort) => {
         const direction = sort.desc ? 'DESC' : 'ASC';
@@ -388,9 +401,44 @@ export function PaginatedListDataTable<
           }
         : undefined;
 
+    function refetchPaginatedList() {
+        queryClient.invalidateQueries({ queryKey });
+    }
+
+    registerRefresher?.(refetchPaginatedList);
+
+    // First we get info on _all_ the fields, including all custom fields, for the
+    // purpose of configuring the table columns.
+    const fields = useListQueryFields(extendedListQuery);
+    const paginatedListObjectPath = getObjectPathToPaginatedList(extendedListQuery);
+
+    const { columns, customFieldColumnNames } = useGeneratedColumns({
+        fields,
+        customizeColumns,
+        rowActions,
+        bulkActions,
+        deleteMutation,
+        additionalColumns,
+        defaultColumnOrder,
+    });
+
+    const columnVisibility = getColumnVisibility(fields, defaultVisibility, customFieldColumnNames);
+    // Get the actual visible columns and only fetch those
+    const visibleColumns = columns
+        // Filter out invisible columns, but _always_ select "id"
+        // because it is usually needed.
+        .filter(c => columnVisibility[c.id as string] || c.id === 'id')
+        .map(c => ({
+            name: c.id as string,
+            isCustomField: (c.meta as any)?.isCustomField ?? false,
+            dependencies: (c.meta as any)?.dependencies ?? [],
+        }));
+    const minimalListQuery = includeOnlySelectedListFields(extendedListQuery, visibleColumns);
+
     const defaultQueryKey = [
         PaginatedListDataTableKey,
-        extendedListQuery,
+        minimalListQuery,
+        visibleColumns,
         page,
         itemsPerPage,
         sorting,
@@ -399,12 +447,6 @@ export function PaginatedListDataTable<
     ];
     const queryKey = transformQueryKey ? transformQueryKey(defaultQueryKey) : defaultQueryKey;
 
-    function refetchPaginatedList() {
-        queryClient.invalidateQueries({ queryKey });
-    }
-
-    registerRefresher?.(refetchPaginatedList);
-
     const { data, isFetching } = useQuery({
         queryFn: () => {
             const searchFilter = onSearchTermChange ? onSearchTermChange(debouncedSearchTerm) : {};
@@ -419,31 +461,16 @@ export function PaginatedListDataTable<
             } as V;
 
             const transformedVariables = transformVariables ? transformVariables(variables) : variables;
-            return api.query(extendedListQuery, transformedVariables);
+            return api.query(minimalListQuery, transformedVariables);
         },
         queryKey,
         placeholderData: keepPreviousData,
     });
-
-    const fields = useListQueryFields(extendedListQuery);
-    const paginatedListObjectPath = getObjectPathToPaginatedList(extendedListQuery);
-
     let listData = data as any;
     for (const path of paginatedListObjectPath) {
         listData = listData?.[path];
     }
 
-    const { columns, customFieldColumnNames } = useGeneratedColumns({
-        fields,
-        customizeColumns,
-        rowActions,
-        bulkActions,
-        deleteMutation,
-        additionalColumns,
-        defaultColumnOrder,
-    });
-
-    const columnVisibility = getColumnVisibility(fields, defaultVisibility, customFieldColumnNames);
     const transformedData =
         typeof transformData === 'function' ? transformData(listData?.items ?? []) : (listData?.items ?? []);
     return (

+ 319 - 9
packages/dashboard/src/lib/framework/document-introspection/add-custom-fields.spec.ts

@@ -1,14 +1,7 @@
 import { CustomFieldConfig, CustomFields } from '@vendure/common/lib/generated-types';
 import { graphql } from 'gql.tada';
-import {
-    DocumentNode,
-    FieldNode,
-    FragmentDefinitionNode,
-    Kind,
-    OperationDefinitionNode,
-    print,
-} from 'graphql';
-import { describe, it, expect, beforeEach } from 'vitest';
+import { DocumentNode, FieldNode, FragmentDefinitionNode, Kind, print } from 'graphql';
+import { beforeEach, describe, expect, it } from 'vitest';
 
 import { addCustomFields } from './add-custom-fields.js';
 
@@ -239,4 +232,321 @@ describe('addCustomFields()', () => {
             addsCustomFieldsToType('Address', addressFragment);
         });
     });
+
+    describe('Nested entity handling', () => {
+        it('User example: Should not add custom fields to Asset fragment used in nested featuredAsset', () => {
+            const assetFragment = graphql(`
+                fragment Asset on Asset {
+                    id
+                    createdAt
+                    updatedAt
+                    name
+                    fileSize
+                    mimeType
+                    type
+                    preview
+                    source
+                    width
+                    height
+                    focalPoint {
+                        x
+                        y
+                    }
+                }
+            `);
+
+            const documentNode = graphql(
+                `
+                    query CollectionList($options: CollectionListOptions) {
+                        collections(options: $options) {
+                            items {
+                                id
+                                createdAt
+                                updatedAt
+                                featuredAsset {
+                                    ...Asset
+                                }
+                                name
+                                slug
+                                breadcrumbs {
+                                    id
+                                    name
+                                    slug
+                                }
+                                children {
+                                    id
+                                    name
+                                }
+                                position
+                                isPrivate
+                                parentId
+                                productVariants {
+                                    totalItems
+                                }
+                            }
+                            totalItems
+                        }
+                    }
+                `,
+                [assetFragment],
+            );
+
+            const customFieldsConfig = new Map<string, CustomFieldConfig[]>();
+            customFieldsConfig.set('Collection', [
+                { name: 'featuredProducts', type: 'relation', list: true, scalarFields: ['id', 'name'] },
+                { name: 'alternativeAsset', type: 'relation', list: false, scalarFields: ['id', 'name'] },
+            ]);
+            customFieldsConfig.set('Asset', [
+                { name: 'assetCustomField1', type: 'string', list: false },
+                { name: 'assetCustomField2', type: 'string', list: false },
+                { name: 'assetCustomField3', type: 'string', list: false },
+            ]);
+
+            const result = addCustomFields(documentNode, { customFieldsMap: customFieldsConfig });
+            const printed = print(result);
+
+            // Should add customFields ONLY to Collection items (top-level entity)
+            expect(printed).toContain('customFields {');
+            expect(printed).toContain('featuredProducts {');
+            expect(printed).toContain('alternativeAsset {');
+
+            // Should NOT add customFields to Asset fragment (only used in nested context)
+            const fragmentMatch = printed.match(/fragment Asset on Asset\s*\{[^}]*\}/s);
+            expect(fragmentMatch).toBeTruthy();
+            expect(fragmentMatch![0]).not.toContain('customFields');
+            expect(fragmentMatch![0]).not.toContain('assetCustomField1');
+
+            // Should NOT add customFields to children (nested Collection entities)
+            const childrenMatch = printed.match(/children\s*\{[^}]+\}/s);
+            expect(childrenMatch).toBeTruthy();
+            expect(childrenMatch![0]).not.toContain('customFields');
+
+            // Should NOT add customFields to breadcrumbs
+            const breadcrumbsMatch = printed.match(/breadcrumbs\s*\{[^}]+\}/s);
+            expect(breadcrumbsMatch).toBeTruthy();
+            expect(breadcrumbsMatch![0]).not.toContain('customFields');
+        });
+
+        it('Should only add custom fields to top-level entity, not nested related entities', () => {
+            const documentNode = graphql(`
+                query CollectionList($options: CollectionListOptions) {
+                    collections(options: $options) {
+                        items {
+                            id
+                            name
+                            slug
+                            featuredAsset {
+                                id
+                                preview
+                            }
+                            children {
+                                id
+                                name
+                                slug
+                            }
+                            breadcrumbs {
+                                id
+                                name
+                                slug
+                            }
+                        }
+                        totalItems
+                    }
+                }
+            `);
+
+            const customFieldsConfig = new Map<string, CustomFieldConfig[]>();
+            customFieldsConfig.set('Collection', [
+                { name: 'featuredProducts', type: 'relation', list: true, scalarFields: ['id', 'name'] },
+                { name: 'alternativeAsset', type: 'relation', list: false, scalarFields: ['id', 'name'] },
+            ]);
+            customFieldsConfig.set('Asset', [
+                { name: 'assetCustomField1', type: 'string', list: false },
+                { name: 'assetCustomField2', type: 'string', list: false },
+            ]);
+
+            const result = addCustomFields(documentNode, { customFieldsMap: customFieldsConfig });
+            const printed = print(result);
+
+            // Should add customFields to top-level Collection (items)
+            expect(printed).toContain('customFields {');
+            expect(printed).toContain('featuredProducts {');
+            expect(printed).toContain('alternativeAsset {');
+
+            // Should NOT add customFields to nested Collection entities (children, breadcrumbs)
+            const childrenMatch = printed.match(/children\s*\{[^}]+\}/s);
+            const breadcrumbsMatch = printed.match(/breadcrumbs\s*\{[^}]+\}/s);
+
+            expect(childrenMatch).toBeTruthy();
+            expect(breadcrumbsMatch).toBeTruthy();
+            expect(childrenMatch![0]).not.toContain('customFields');
+            expect(breadcrumbsMatch![0]).not.toContain('customFields');
+
+            // Should NOT add customFields to nested Asset entity (featuredAsset)
+            const featuredAssetMatch = printed.match(/featuredAsset\s*\{[^}]+\}/s);
+            expect(featuredAssetMatch).toBeTruthy();
+            expect(featuredAssetMatch![0]).not.toContain('customFields');
+            expect(featuredAssetMatch![0]).not.toContain('assetCustomField1');
+        });
+
+        it('Should NOT add custom fields to fragments that are only used in nested contexts', () => {
+            const assetFragment = graphql(`
+                fragment Asset on Asset {
+                    id
+                    name
+                    preview
+                }
+            `);
+
+            const documentNode = graphql(
+                `
+                    query ProductList($options: ProductListOptions) {
+                        products(options: $options) {
+                            items {
+                                id
+                                name
+                                featuredAsset {
+                                    ...Asset
+                                }
+                                variants {
+                                    id
+                                    name
+                                    assets {
+                                        ...Asset
+                                    }
+                                }
+                            }
+                            totalItems
+                        }
+                    }
+                `,
+                [assetFragment],
+            );
+
+            const customFieldsConfig = new Map<string, CustomFieldConfig[]>();
+            customFieldsConfig.set('Product', [{ name: 'productCustomField', type: 'string', list: false }]);
+            customFieldsConfig.set('Asset', [{ name: 'assetCustomField', type: 'string', list: false }]);
+            customFieldsConfig.set('ProductVariant', [
+                { name: 'variantCustomField', type: 'string', list: false },
+            ]);
+
+            const result = addCustomFields(documentNode, { customFieldsMap: customFieldsConfig });
+            const printed = print(result);
+
+            // Should add customFields to Product (top-level query entity)
+            expect(printed).toContain('customFields {');
+            expect(printed).toContain('productCustomField');
+
+            // Should NOT add customFields to Asset fragment (only used in nested contexts)
+            expect(printed).not.toMatch(/fragment Asset on Asset\s*\{[^}]*customFields/s);
+
+            // Should NOT add customFields to nested ProductVariant entities
+            const variantsMatch = printed.match(/variants\s*\{[^}]+\}/s);
+            expect(variantsMatch).toBeTruthy();
+            expect(variantsMatch![0]).not.toContain('customFields');
+            expect(variantsMatch![0]).not.toContain('variantCustomField');
+        });
+
+        it('Should add custom fields to fragments used at top level', () => {
+            const productFragment = graphql(`
+                fragment ProductDetails on Product {
+                    id
+                    name
+                    slug
+                }
+            `);
+
+            const documentNode = graphql(
+                `
+                    query ProductList($options: ProductListOptions) {
+                        products(options: $options) {
+                            items {
+                                ...ProductDetails
+                                featuredAsset {
+                                    id
+                                    preview
+                                }
+                            }
+                            totalItems
+                        }
+                    }
+                `,
+                [productFragment],
+            );
+
+            const customFieldsConfig = new Map<string, CustomFieldConfig[]>();
+            customFieldsConfig.set('Product', [{ name: 'productCustomField', type: 'string', list: false }]);
+            customFieldsConfig.set('Asset', [{ name: 'assetCustomField', type: 'string', list: false }]);
+
+            const result = addCustomFields(documentNode, { customFieldsMap: customFieldsConfig });
+            const printed = print(result);
+
+            // Should add customFields to ProductDetails fragment (used at top level in items)
+            expect(printed).toContain('fragment ProductDetails on Product');
+            expect(printed).toContain('productCustomField');
+
+            // Should NOT add customFields to featuredAsset (nested entity)
+            const featuredAssetMatch = printed.match(/featuredAsset\s*\{[^}]+\}/s);
+            expect(featuredAssetMatch).toBeTruthy();
+            expect(featuredAssetMatch![0]).not.toContain('customFields');
+        });
+
+        it('Should handle complex nested structure with multiple entity types', () => {
+            const documentNode = graphql(`
+                query ComplexQuery {
+                    orders {
+                        items {
+                            id
+                            code
+                            customer {
+                                id
+                                firstName
+                                addresses {
+                                    id
+                                    streetLine1
+                                    country {
+                                        id
+                                        name
+                                    }
+                                }
+                            }
+                            lines {
+                                id
+                                productVariant {
+                                    id
+                                    name
+                                    product {
+                                        id
+                                        name
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            `);
+
+            const customFieldsConfig = new Map<string, CustomFieldConfig[]>();
+            customFieldsConfig.set('Order', [{ name: 'orderCustomField', type: 'string', list: false }]);
+            customFieldsConfig.set('Customer', [
+                { name: 'customerCustomField', type: 'string', list: false },
+            ]);
+            customFieldsConfig.set('Address', [{ name: 'addressCustomField', type: 'string', list: false }]);
+            customFieldsConfig.set('Product', [{ name: 'productCustomField', type: 'string', list: false }]);
+
+            const result = addCustomFields(documentNode, { customFieldsMap: customFieldsConfig });
+            const printed = print(result);
+
+            // Should only add customFields to top-level Order entity
+            expect(printed).toContain('customFields {');
+            expect(printed).toContain('orderCustomField');
+
+            // Should NOT add customFields to any nested entities
+            expect(printed).not.toMatch(/customer\s*\{[^}]*customFields/s);
+            expect(printed).not.toMatch(/addresses\s*\{[^}]*customFields/s);
+            expect(printed).not.toMatch(/country\s*\{[^}]*customFields/s);
+            expect(printed).not.toMatch(/productVariant\s*\{[^}]*customFields/s);
+            expect(printed).not.toMatch(/product\s*\{[^}]*customFields/s);
+        });
+    });
 });

+ 60 - 31
packages/dashboard/src/lib/framework/document-introspection/add-custom-fields.ts

@@ -114,25 +114,75 @@ export function addCustomFields<T, V extends Variables = Variables>(
     const customFields = options?.customFieldsMap || globalCustomFieldsMap;
 
     const targetNodes: Array<{ typeName: string; selectionSet: SelectionSetNode }> = [];
+    const topLevelFragments = new Set<string>();
 
-    const fragmentDefs = clone.definitions.filter(isFragmentDefinition);
-    for (const fragmentDef of fragmentDefs) {
-        targetNodes.push({
-            typeName: fragmentDef.typeCondition.name.value,
-            selectionSet: fragmentDef.selectionSet,
-        });
-    }
+    // First, identify which fragments are used at the top level (directly in items or in the main query)
     const queryDefs = clone.definitions.filter(isOperationDefinition);
 
     for (const queryDef of queryDefs) {
         const typeInfo = getOperationTypeInfo(queryDef);
         const fieldNode = queryDef.selectionSet.selections[0] as FieldNode;
         if (typeInfo && fieldNode?.selectionSet) {
+            let topLevelSelectionSet: SelectionSetNode | undefined;
+
+            // For paginated list queries, find the items field and add custom fields to its entity type
+            if (typeInfo.isPaginatedList) {
+                const itemsField = fieldNode.selectionSet.selections.find(
+                    sel => sel.kind === 'Field' && sel.name.value === 'items',
+                ) as FieldNode | undefined;
+
+                if (itemsField?.selectionSet) {
+                    // For paginated lists, the type is like "ProductList" but we need "Product"
+                    const entityTypeName = typeInfo.type.replace(/List$/, '');
+                    targetNodes.push({
+                        typeName: entityTypeName,
+                        selectionSet: itemsField.selectionSet,
+                    });
+                    topLevelSelectionSet = itemsField.selectionSet;
+                }
+            } else {
+                // For single entity queries, add custom fields to the top-level entity
+                targetNodes.push({
+                    typeName: typeInfo.type,
+                    selectionSet: fieldNode.selectionSet,
+                });
+                topLevelSelectionSet = fieldNode.selectionSet;
+            }
+
+            // Track which fragments are used at the top level (not in nested entities)
+            if (topLevelSelectionSet) {
+                for (const selection of topLevelSelectionSet.selections) {
+                    if (selection.kind === Kind.FRAGMENT_SPREAD) {
+                        topLevelFragments.add(selection.name.value);
+                    }
+                }
+            }
+        }
+    }
+
+    // Now add fragments
+    const fragmentDefs = clone.definitions.filter(isFragmentDefinition);
+
+    // Check if this document has query definitions - if not, add all fragments
+    const hasQueries = queryDefs.length > 0;
+
+    for (const fragmentDef of fragmentDefs) {
+        if (hasQueries) {
+            // If we have queries, only add custom fields to fragments used at the top level
+            // Skip fragments that are only used in nested contexts
+            if (topLevelFragments.has(fragmentDef.name.value)) {
+                targetNodes.push({
+                    typeName: fragmentDef.typeCondition.name.value,
+                    selectionSet: fragmentDef.selectionSet,
+                });
+            }
+        } else {
+            // For standalone fragments (no queries), add custom fields to all fragments
+            // since we don't know where they'll be used
             targetNodes.push({
-                typeName: typeInfo.type,
-                selectionSet: fieldNode.selectionSet,
+                typeName: fragmentDef.typeCondition.name.value,
+                selectionSet: fragmentDef.selectionSet,
             });
-            addTargetNodesRecursively(fieldNode.selectionSet, typeInfo.type, targetNodes);
         }
     }
 
@@ -271,24 +321,3 @@ function isOperationDefinition(value: DefinitionNode): value is OperationDefinit
 function isFieldNode(value: SelectionNode): value is FieldNode {
     return value.kind === Kind.FIELD;
 }
-
-function addTargetNodesRecursively(
-    selectionSet: SelectionSetNode,
-    parentTypeName: string,
-    targetNodes: Array<{ typeName: string; selectionSet: SelectionSetNode }>,
-) {
-    for (const selection of selectionSet.selections) {
-        if (selection.kind === 'Field' && selection.selectionSet) {
-            const fieldNode = selection;
-            const typeInfo = getOperationTypeInfo(fieldNode, parentTypeName); // Assuming this function can handle FieldNode
-            if (typeInfo && fieldNode.selectionSet) {
-                targetNodes.push({
-                    typeName: typeInfo.type,
-                    selectionSet: fieldNode.selectionSet,
-                });
-                // Recursively process the selection set of the current field
-                addTargetNodesRecursively(fieldNode.selectionSet, typeInfo.type, targetNodes);
-            }
-        }
-    }
-}

+ 1 - 159
packages/dashboard/src/lib/framework/document-introspection/get-document-structure.spec.ts

@@ -8,165 +8,7 @@ import {
 } from './get-document-structure.js';
 
 vi.mock('virtual:admin-api-schema', () => {
-    return {
-        schemaInfo: {
-            types: {
-                Query: {
-                    products: ['ProductList', false, false, true],
-                    product: ['Product', false, false, false],
-                    collection: ['Collection', false, false, false],
-                    order: ['Order', false, false, false],
-                },
-                Mutation: {
-                    updateProduct: ['Product', false, false, false],
-                    adjustDraftOrderLine: ['Order', false, false, false],
-                },
-
-                Collection: {
-                    id: ['ID', false, false, false],
-                    name: ['String', false, false, false],
-                    productVariants: ['ProductVariantList', false, false, true],
-                },
-
-                ProductVariantList: {
-                    items: ['ProductVariant', false, true, false],
-                    totalItems: ['Int', false, false, false],
-                },
-
-                Product: {
-                    channels: ['Channel', false, true, false],
-                    id: ['ID', false, false, false],
-                    createdAt: ['DateTime', false, false, false],
-                    updatedAt: ['DateTime', false, false, false],
-                    languageCode: ['LanguageCode', false, false, false],
-                    name: ['String', false, false, false],
-                    slug: ['String', false, false, false],
-                    description: ['String', false, false, false],
-                    enabled: ['Boolean', false, false, false],
-                    featuredAsset: ['Asset', true, false, false],
-                    assets: ['Asset', false, true, false],
-                    variants: ['ProductVariant', false, true, false],
-                    variantList: ['ProductVariantList', false, false, true],
-                    optionGroups: ['ProductOptionGroup', false, true, false],
-                    facetValues: ['FacetValue', false, true, false],
-                    translations: ['ProductTranslation', false, true, false],
-                    collections: ['Collection', false, true, false],
-                    reviews: ['ProductReviewList', false, false, true],
-                    reviewsHistogram: ['ProductReviewHistogramItem', false, true, false],
-                    customFields: ['ProductCustomFields', true, false, false],
-                },
-                ProductVariantPrice: {
-                    currencyCode: ['CurrencyCode', false, false, false],
-                    price: ['Money', false, false, false],
-                    customFields: ['JSON', true, false, false],
-                },
-                ProductVariant: {
-                    enabled: ['Boolean', false, false, false],
-                    trackInventory: ['GlobalFlag', false, false, false],
-                    stockOnHand: ['Int', false, false, false],
-                    stockAllocated: ['Int', false, false, false],
-                    outOfStockThreshold: ['Int', false, false, false],
-                    useGlobalOutOfStockThreshold: ['Boolean', false, false, false],
-                    prices: ['ProductVariantPrice', false, true, false],
-                    stockLevels: ['StockLevel', false, true, false],
-                    stockMovements: ['StockMovementList', false, false, false],
-                    channels: ['Channel', false, true, false],
-                    id: ['ID', false, false, false],
-                    product: ['Product', false, false, false],
-                    productId: ['ID', false, false, false],
-                    createdAt: ['DateTime', false, false, false],
-                    updatedAt: ['DateTime', false, false, false],
-                    languageCode: ['LanguageCode', false, false, false],
-                    sku: ['String', false, false, false],
-                    name: ['String', false, false, false],
-                    featuredAsset: ['Asset', true, false, false],
-                    assets: ['Asset', false, true, false],
-                    price: ['Money', false, false, false],
-                    currencyCode: ['CurrencyCode', false, false, false],
-                    priceWithTax: ['Money', false, false, false],
-                    stockLevel: ['String', false, false, false],
-                    taxRateApplied: ['TaxRate', false, false, false],
-                    taxCategory: ['TaxCategory', false, false, false],
-                    options: ['ProductOption', false, true, false],
-                    facetValues: ['FacetValue', false, true, false],
-                    translations: ['ProductVariantTranslation', false, true, false],
-                    customFields: ['JSON', true, false, false],
-                },
-                ProductCustomFields: {
-                    custom1: ['String', false, false, false],
-                },
-
-                Asset: {
-                    id: ['ID', false, false, false],
-                    createdAt: ['DateTime', false, false, false],
-                    updatedAt: ['DateTime', false, false, false],
-                    name: ['String', false, false, false],
-                    type: ['AssetType', false, false, false],
-                    fileSize: ['Int', false, false, false],
-                    mimeType: ['String', false, false, false],
-                    width: ['Int', false, false, false],
-                    height: ['Int', false, false, false],
-                    source: ['String', false, false, false],
-                    preview: ['String', false, false, false],
-                    focalPoint: ['Coordinate', true, false, false],
-                    tags: ['Tag', false, true, false],
-                    customFields: ['JSON', true, false, false],
-                },
-                ProductTranslation: {
-                    id: ['ID', false, false, false],
-                    createdAt: ['DateTime', false, false, false],
-                    updatedAt: ['DateTime', false, false, false],
-                    languageCode: ['LanguageCode', false, false, false],
-                    name: ['String', false, false, false],
-                    slug: ['String', false, false, false],
-                    description: ['String', false, false, false],
-                    customFields: ['ProductTranslationCustomFields', true, false, false],
-                },
-                ProductList: {
-                    items: ['Product', false, true, false],
-                    totalItems: ['Int', false, false, false],
-                },
-
-                ProductVariantTranslation: {
-                    id: ['ID', false, false, false],
-                    createdAt: ['DateTime', false, false, false],
-                    updatedAt: ['DateTime', false, false, false],
-                    languageCode: ['LanguageCode', false, false, false],
-                    name: ['String', false, false, false],
-                },
-                Order: {
-                    id: ['ID', false, false, false],
-                    lines: ['OrderLine', false, true, false],
-                },
-                OrderLine: {
-                    id: ['ID', false, false, false],
-                    quantity: ['Int', false, false, false],
-                },
-            },
-            inputs: {
-                UpdateProductInput: {
-                    id: ['ID', false, false, false],
-                    name: ['String', false, false, false],
-                },
-                AdjustDraftOrderLineInput: {
-                    orderLineId: ['ID', false, false, false],
-                    quantity: ['Int', false, false, false],
-                },
-            },
-            scalars: [
-                'ID',
-                'String',
-                'Int',
-                'Boolean',
-                'Float',
-                'JSON',
-                'DateTime',
-                'Upload',
-                'CurrencyCode',
-            ],
-            enums: {},
-        },
-    };
+    return import('./testing-utils.js').then(m => m.getMockSchemaInfo());
 });
 
 describe('getListQueryFields', () => {

+ 1840 - 0
packages/dashboard/src/lib/framework/document-introspection/include-only-selected-list-fields.spec.ts

@@ -0,0 +1,1840 @@
+import { DocumentNode, parse, print } from 'graphql';
+import { describe, expect, it, vi } from 'vitest';
+
+import { includeOnlySelectedListFields } from './include-only-selected-list-fields.js';
+
+vi.mock('virtual:admin-api-schema', () => {
+    return import('./testing-utils.js').then(m => m.getMockSchemaInfo());
+});
+
+describe('includeOnlySelectedListFields', () => {
+    const createTestDocument = (itemsFields: string): DocumentNode => {
+        return parse(`
+            query ProductList($options: ProductListOptions) {
+                products(options: $options) {
+                    items {
+                        ${itemsFields}
+                    }
+                    totalItems
+                }
+            }
+        `);
+    };
+
+    const normalizeQuery = (query: string): string => {
+        // Remove extra whitespace and normalize for comparison
+        return query.replace(/\s+/g, ' ').trim();
+    };
+
+    describe('basic field selection', () => {
+        it('should return original document when no columns are selected', () => {
+            const document = createTestDocument('id name slug');
+            const result = includeOnlySelectedListFields(document, []);
+
+            expect(print(result)).toEqual(print(document));
+        });
+
+        it('should filter to only selected fields', () => {
+            const document = createTestDocument(`
+                id
+                name
+                slug
+                enabled
+                createdAt
+                updatedAt
+            `);
+
+            const result = includeOnlySelectedListFields(document, [
+                { name: 'id', isCustomField: false },
+                { name: 'name', isCustomField: false },
+            ]);
+
+            const resultQuery = normalizeQuery(print(result));
+            expect(resultQuery).toContain('items { id name }');
+            expect(resultQuery).not.toContain('slug');
+            expect(resultQuery).not.toContain('enabled');
+            expect(resultQuery).not.toContain('createdAt');
+            expect(resultQuery).not.toContain('updatedAt');
+        });
+
+        it('should handle single field selection', () => {
+            const document = createTestDocument(`
+                id
+                name
+                slug
+                enabled
+            `);
+
+            const result = includeOnlySelectedListFields(document, [{ name: 'name', isCustomField: false }]);
+
+            const resultQuery = normalizeQuery(print(result));
+            expect(resultQuery).toContain('items { name }');
+            expect(resultQuery).not.toContain('slug');
+            expect(resultQuery).not.toContain('enabled');
+        });
+
+        it('should preserve nested field structures', () => {
+            const document = createTestDocument(`
+                id
+                name
+                featuredAsset {
+                    id
+                    preview
+                    source
+                }
+                slug
+            `);
+
+            const result = includeOnlySelectedListFields(document, [
+                { name: 'id', isCustomField: false },
+                { name: 'featuredAsset', isCustomField: false },
+            ]);
+
+            const resultQuery = normalizeQuery(print(result));
+            expect(resultQuery).toContain('items { id featuredAsset { id preview source } }');
+            expect(resultQuery).not.toContain('name');
+            expect(resultQuery).not.toContain('slug');
+        });
+
+        it('should preserve __typename if present in original', () => {
+            const document = createTestDocument(`
+                __typename
+                id
+                name
+                slug
+            `);
+
+            const result = includeOnlySelectedListFields(document, [{ name: 'name', isCustomField: false }]);
+
+            const resultQuery = normalizeQuery(print(result));
+            expect(resultQuery).toContain('__typename');
+            expect(resultQuery).toContain('name');
+            expect(resultQuery).not.toContain('slug');
+        });
+    });
+
+    describe('custom fields handling', () => {
+        it('should include custom fields when specified', () => {
+            const document = createTestDocument(`
+                id
+                name
+                customFields {
+                    shortDescription
+                    isEcoFriendly
+                    warrantyMonths
+                }
+            `);
+
+            const result = includeOnlySelectedListFields(document, [
+                { name: 'id', isCustomField: false },
+                { name: 'shortDescription', isCustomField: true },
+                { name: 'isEcoFriendly', isCustomField: true },
+            ]);
+
+            const resultQuery = normalizeQuery(print(result));
+            expect(resultQuery).toContain('items { id customFields { shortDescription isEcoFriendly } }');
+            expect(resultQuery).not.toContain('warrantyMonths');
+            expect(resultQuery).not.toContain('name');
+        });
+
+        it('should exclude customFields entirely if no custom fields are selected', () => {
+            const document = createTestDocument(`
+                id
+                name
+                customFields {
+                    shortDescription
+                    isEcoFriendly
+                }
+            `);
+
+            const result = includeOnlySelectedListFields(document, [
+                { name: 'id', isCustomField: false },
+                { name: 'name', isCustomField: false },
+            ]);
+
+            const resultQuery = normalizeQuery(print(result));
+            expect(resultQuery).toContain('items { id name }');
+            expect(resultQuery).not.toContain('customFields');
+        });
+
+        it('should handle mixed regular and custom field selection', () => {
+            const document = createTestDocument(`
+                id
+                name
+                slug
+                enabled
+                customFields {
+                    shortDescription
+                    warrantyMonths
+                    isEcoFriendly
+                }
+            `);
+
+            const result = includeOnlySelectedListFields(document, [
+                { name: 'name', isCustomField: false },
+                { name: 'enabled', isCustomField: false },
+                { name: 'shortDescription', isCustomField: true },
+            ]);
+
+            const resultQuery = normalizeQuery(print(result));
+            expect(resultQuery).toContain('name');
+            expect(resultQuery).toContain('enabled');
+            expect(resultQuery).toContain('customFields { shortDescription }');
+            expect(resultQuery).not.toContain('warrantyMonths');
+            expect(resultQuery).not.toContain('isEcoFriendly');
+            expect(resultQuery).not.toContain('slug');
+        });
+    });
+
+    describe('fragment handling', () => {
+        it('should preserve inline fragments', () => {
+            const document = parse(`
+                query ProductList($options: ProductListOptions) {
+                    products(options: $options) {
+                        items {
+                            id
+                            name
+                            ... on Product {
+                                slug
+                                description
+                            }
+                        }
+                        totalItems
+                    }
+                }
+            `);
+
+            const result = includeOnlySelectedListFields(document, [
+                { name: 'id', isCustomField: false },
+                { name: 'name', isCustomField: false },
+            ]);
+
+            const resultQuery = normalizeQuery(print(result));
+            expect(resultQuery).toContain('... on Product');
+        });
+
+        it('should preserve fragment spreads when they contain selected fields', () => {
+            const document = parse(`
+                query ProductList($options: ProductListOptions) {
+                    products(options: $options) {
+                        items {
+                            id
+                            ...ProductFields
+                        }
+                        totalItems
+                    }
+                }
+
+                fragment ProductFields on Product {
+                    name
+                    slug
+                    enabled
+                }
+            `);
+
+            const result = includeOnlySelectedListFields(document, [
+                { name: 'id', isCustomField: false },
+                { name: 'name', isCustomField: false },
+            ]);
+
+            const resultQuery = normalizeQuery(print(result));
+            expect(resultQuery).toContain('...ProductFields');
+            expect(resultQuery).toContain('fragment ProductFields');
+            expect(resultQuery).toContain(' name');
+            // Fragment should be filtered to only include selected fields
+            expect(resultQuery).not.toContain('slug');
+            expect(resultQuery).not.toContain('enabled');
+        });
+    });
+
+    describe('edge cases', () => {
+        it('should add id field if no fields selected to maintain valid query', () => {
+            const document = createTestDocument(`
+                id
+                name
+                slug
+            `);
+
+            // Select a field that doesn't exist in the document
+            const result = includeOnlySelectedListFields(document, [
+                { name: 'nonExistentField', isCustomField: false },
+            ]);
+
+            const resultQuery = normalizeQuery(print(result));
+            expect(resultQuery).toContain('items { id }');
+            expect(resultQuery).not.toContain('name');
+            expect(resultQuery).not.toContain('slug');
+        });
+
+        it('should handle document without items selectionSet', () => {
+            const document = parse(`
+                query ProductList($options: ProductListOptions) {
+                    products(options: $options) {
+                        items
+                        totalItems
+                    }
+                }
+            `);
+
+            const result = includeOnlySelectedListFields(document, [
+                { name: 'id', isCustomField: false },
+                { name: 'name', isCustomField: false },
+            ]);
+
+            const resultQuery = normalizeQuery(print(result));
+            expect(resultQuery).toContain('items');
+            expect(resultQuery).toContain('totalItems');
+        });
+
+        it('should handle multiple queries in document', () => {
+            const document = parse(`
+                query ProductList($options: ProductListOptions) {
+                    products(options: $options) {
+                        items {
+                            id
+                            name
+                            slug
+                        }
+                        totalItems
+                    }
+                }
+
+                query ProductCount {
+                    products {
+                        totalItems
+                    }
+                }
+            `);
+
+            const result = includeOnlySelectedListFields(document, [{ name: 'id', isCustomField: false }]);
+
+            // Should only modify the first query's items field
+            const resultQuery = normalizeQuery(print(result));
+            expect(resultQuery).toContain('query ProductList');
+            expect(resultQuery).toContain('query ProductCount');
+        });
+
+        it('should handle deeply nested structures', () => {
+            const document = createTestDocument(`
+                id
+                name
+                slug
+                variants {
+                    id
+                    name
+                    options {
+                        id
+                        code
+                        name
+                    }
+                }
+            `);
+
+            const result = includeOnlySelectedListFields(document, [
+                { name: 'id', isCustomField: false },
+                { name: 'variants', isCustomField: false },
+            ]);
+
+            const resultQuery = normalizeQuery(print(result));
+            expect(resultQuery).toContain('variants');
+            expect(resultQuery).toContain('options');
+            // The nested name fields within variants are preserved, but root level name and slug should not be
+            expect(resultQuery).not.toContain('slug');
+            // Check that the structure is preserved correctly
+            expect(resultQuery).toContain('variants { id name options');
+        });
+
+        it('should preserve totalItems and other pagination fields', () => {
+            const document = parse(`
+                query ProductList($options: ProductListOptions) {
+                    products(options: $options) {
+                        items {
+                            id
+                            name
+                            slug
+                        }
+                        totalItems
+                        hasNextPage
+                        hasPreviousPage
+                    }
+                }
+            `);
+
+            const result = includeOnlySelectedListFields(document, [{ name: 'id', isCustomField: false }]);
+
+            const resultQuery = normalizeQuery(print(result));
+            expect(resultQuery).toContain('totalItems');
+            expect(resultQuery).toContain('hasNextPage');
+            expect(resultQuery).toContain('hasPreviousPage');
+            expect(resultQuery).toContain('items { id }');
+            expect(resultQuery).not.toContain('name');
+            expect(resultQuery).not.toContain('slug');
+        });
+
+        it('should handle empty customFields selection gracefully', () => {
+            const document = createTestDocument(`
+                id
+                name
+                customFields {
+                    field1
+                    field2
+                }
+            `);
+
+            const result = includeOnlySelectedListFields(document, [
+                { name: 'id', isCustomField: false },
+                { name: 'nonExistentCustomField', isCustomField: true },
+            ]);
+
+            const resultQuery = normalizeQuery(print(result));
+            expect(resultQuery).toContain('items { id }');
+            expect(resultQuery).not.toContain('customFields');
+            expect(resultQuery).not.toContain('name');
+        });
+
+        it('should handle queries with aliases', () => {
+            const document = parse(`
+                query ProductList($options: ProductListOptions) {
+                    allProducts: products(options: $options) {
+                        items {
+                            productId: id
+                            productName: name
+                            slug
+                        }
+                        totalItems
+                    }
+                }
+            `);
+
+            const result = includeOnlySelectedListFields(document, [
+                { name: 'id', isCustomField: false },
+                { name: 'name', isCustomField: false },
+            ]);
+
+            const resultQuery = normalizeQuery(print(result));
+            // Note: aliases make this more complex - the function looks at field names
+            expect(resultQuery).toContain('productId: id');
+            expect(resultQuery).toContain('productName: name');
+            expect(resultQuery).not.toContain('slug');
+        });
+    });
+
+    describe('fragment-based items selection', () => {
+        it('should handle items defined in fragments - user example case', () => {
+            const document = parse(`
+                query FacetList($options: FacetListOptions, $facetValueListOptions: FacetValueListOptions) {
+                    facets(options: $options) {
+                        items {
+                            ...FacetWithValueList
+                        }
+                        totalItems
+                    }
+                }
+
+                fragment FacetWithValueList on Facet {
+                    id
+                    createdAt
+                    updatedAt
+                    name
+                    code
+                    isPrivate
+                    valueList(options: $facetValueListOptions) {
+                        totalItems
+                        items {
+                            ...FacetValue
+                        }
+                    }
+                }
+
+                fragment FacetValue on FacetValue {
+                    id
+                    createdAt
+                    updatedAt
+                    languageCode
+                    code
+                    name
+                    translations {
+                        id
+                        languageCode
+                        name
+                    }
+                    facet {
+                        id
+                        createdAt
+                        updatedAt
+                        name
+                        code
+                    }
+                }
+            `);
+
+            const result = includeOnlySelectedListFields(document, [
+                { name: 'id', isCustomField: false },
+                { name: 'name', isCustomField: false },
+                { name: 'code', isCustomField: false },
+            ]);
+
+            const resultQuery = normalizeQuery(print(result));
+
+            // Should include selected fields
+            expect(resultQuery).toContain('...FacetWithValueList');
+            expect(resultQuery).toContain('fragment FacetWithValueList');
+
+            // Fragment should be filtered to only include selected fields
+            expect(resultQuery).toContain('id');
+            expect(resultQuery).toContain(' name');
+            expect(resultQuery).toContain(' code');
+
+            // Should exclude non-selected fields from fragment
+            expect(resultQuery).not.toContain('createdAt');
+            expect(resultQuery).not.toContain('updatedAt');
+            expect(resultQuery).not.toContain('isPrivate');
+            expect(resultQuery).not.toContain('valueList');
+
+            // Should remove unused FacetValue fragment
+            expect(resultQuery).not.toContain('fragment FacetValue');
+            expect(resultQuery).not.toContain('...FacetValue');
+        });
+
+        it('should handle nested fragments in items fragments', () => {
+            const document = parse(`
+                query ProductList($options: ProductListOptions) {
+                    products(options: $options) {
+                        items {
+                            ...ProductWithAssets
+                        }
+                        totalItems
+                    }
+                }
+
+                fragment ProductWithAssets on Product {
+                    id
+                    name
+                    slug
+                    featuredAsset {
+                        ...AssetInfo
+                    }
+                    assets {
+                        ...AssetInfo
+                    }
+                }
+
+                fragment AssetInfo on Asset {
+                    id
+                    name
+                    preview
+                    source
+                    width
+                    height
+                }
+            `);
+
+            const result = includeOnlySelectedListFields(document, [
+                { name: 'name', isCustomField: false },
+                { name: 'featuredAsset', isCustomField: false },
+            ]);
+
+            const resultQuery = normalizeQuery(print(result));
+
+            // Should include selected fields
+            expect(resultQuery).toContain('...ProductWithAssets');
+            expect(resultQuery).toContain('fragment ProductWithAssets');
+
+            // Should include used nested fragments
+            expect(resultQuery).toContain('fragment AssetInfo');
+            expect(resultQuery).toContain('...AssetInfo');
+
+            // Fragment should be filtered
+            expect(resultQuery).toContain(' name');
+            expect(resultQuery).toContain('featuredAsset');
+
+            // Should exclude non-selected fields
+            expect(resultQuery).not.toContain('slug');
+            expect(resultQuery).not.toContain('assets');
+        });
+
+        it('should handle mixed direct fields and fragment spreads in items', () => {
+            const document = parse(`
+                query ProductList($options: ProductListOptions) {
+                    products(options: $options) {
+                        items {
+                            id
+                            enabled
+                            ...ProductCore
+                            customFields {
+                                shortDescription
+                                warrantyMonths
+                            }
+                        }
+                        totalItems
+                    }
+                }
+
+                fragment ProductCore on Product {
+                    name
+                    slug
+                    description
+                    featuredAsset {
+                        id
+                        preview
+                    }
+                }
+            `);
+
+            const result = includeOnlySelectedListFields(document, [
+                { name: 'id', isCustomField: false },
+                { name: 'name', isCustomField: false },
+                { name: 'featuredAsset', isCustomField: false },
+                { name: 'shortDescription', isCustomField: true },
+            ]);
+
+            const resultQuery = normalizeQuery(print(result));
+
+            // Should include direct fields
+            expect(resultQuery).toContain('id');
+
+            // Should include fragment with filtered content
+            expect(resultQuery).toContain('...ProductCore');
+            expect(resultQuery).toContain('fragment ProductCore');
+            expect(resultQuery).toContain(' name');
+            expect(resultQuery).toContain('featuredAsset');
+
+            // Should include filtered custom fields
+            expect(resultQuery).toContain('customFields { shortDescription }');
+
+            // Should exclude non-selected fields
+            expect(resultQuery).not.toContain('enabled');
+            expect(resultQuery).not.toContain('slug');
+            expect(resultQuery).not.toContain('description');
+            expect(resultQuery).not.toContain('warrantyMonths');
+        });
+
+        it('should handle items with only fragment spreads', () => {
+            const document = parse(`
+                query CustomerList($options: CustomerListOptions) {
+                    customers(options: $options) {
+                        items {
+                            ...CustomerBasic
+                            ...CustomerContact
+                        }
+                        totalItems
+                    }
+                }
+
+                fragment CustomerBasic on Customer {
+                    id
+                    title
+                    firstName
+                    lastName
+                    emailAddress
+                }
+
+                fragment CustomerContact on Customer {
+                    phoneNumber
+                    addresses {
+                        id
+                        streetLine1
+                        city
+                        country {
+                            code
+                            name
+                        }
+                    }
+                }
+            `);
+
+            const result = includeOnlySelectedListFields(document, [
+                { name: 'firstName', isCustomField: false },
+                { name: 'lastName', isCustomField: false },
+                { name: 'emailAddress', isCustomField: false },
+            ]);
+
+            const resultQuery = normalizeQuery(print(result));
+
+            // Should include fragment with selected fields
+            expect(resultQuery).toContain('...CustomerBasic');
+            expect(resultQuery).toContain('fragment CustomerBasic');
+            expect(resultQuery).toContain('firstName');
+            expect(resultQuery).toContain('lastName');
+            expect(resultQuery).toContain('emailAddress');
+
+            // Should exclude unused fragment
+            expect(resultQuery).not.toContain('...CustomerContact');
+            expect(resultQuery).not.toContain('fragment CustomerContact');
+
+            // Should exclude non-selected fields from used fragment
+            expect(resultQuery).not.toContain('title');
+            expect(resultQuery).not.toContain('phoneNumber');
+            expect(resultQuery).not.toContain('addresses');
+        });
+
+        it('should handle deeply nested fragment spreads in items', () => {
+            const document = parse(`
+                query ProductList($options: ProductListOptions) {
+                    products(options: $options) {
+                        items {
+                            ...ProductWithVariants
+                        }
+                        totalItems
+                    }
+                }
+
+                fragment ProductWithVariants on Product {
+                    id
+                    name
+                    variants {
+                        id
+                        name
+                        options {
+                            ...ProductOptionDetail
+                        }
+                    }
+                }
+
+                fragment ProductOptionDetail on ProductOption {
+                    id
+                    code
+                    name
+                    group {
+                        id
+                        name
+                        code
+                    }
+                }
+            `);
+
+            const result = includeOnlySelectedListFields(document, [
+                { name: 'name', isCustomField: false },
+                { name: 'variants', isCustomField: false },
+            ]);
+
+            const resultQuery = normalizeQuery(print(result));
+
+            // Should include main fragment
+            expect(resultQuery).toContain('...ProductWithVariants');
+            expect(resultQuery).toContain('fragment ProductWithVariants');
+
+            // Should include nested structures
+            expect(resultQuery).toContain(' name');
+            expect(resultQuery).toContain('variants');
+            expect(resultQuery).toContain('options');
+
+            // Should include nested fragment
+            expect(resultQuery).toContain('fragment ProductOptionDetail');
+            expect(resultQuery).toContain('...ProductOptionDetail');
+        });
+
+        it('should handle fragment spreads with custom fields in items', () => {
+            const document = parse(`
+                query ProductList($options: ProductListOptions) {
+                    products(options: $options) {
+                        items {
+                            ...ProductWithCustomFields
+                        }
+                        totalItems
+                    }
+                }
+
+                fragment ProductWithCustomFields on Product {
+                    id
+                    name
+                    slug
+                    customFields {
+                        shortDescription
+                        warrantyMonths
+                        isEcoFriendly
+                        featuredCollection {
+                            id
+                            name
+                        }
+                    }
+                }
+            `);
+
+            const result = includeOnlySelectedListFields(document, [
+                { name: 'name', isCustomField: false },
+                { name: 'shortDescription', isCustomField: true },
+                { name: 'isEcoFriendly', isCustomField: true },
+            ]);
+
+            const resultQuery = normalizeQuery(print(result));
+
+            // Should include fragment
+            expect(resultQuery).toContain('...ProductWithCustomFields');
+            expect(resultQuery).toContain('fragment ProductWithCustomFields');
+
+            // Should include selected regular field
+            expect(resultQuery).toContain(' name');
+
+            // Should include filtered custom fields
+            expect(resultQuery).toContain('customFields { shortDescription isEcoFriendly }');
+
+            // Should exclude non-selected fields
+            expect(resultQuery).not.toContain('slug');
+            expect(resultQuery).not.toContain('warrantyMonths');
+            expect(resultQuery).not.toContain('featuredCollection');
+        });
+
+        it('should only filter top-level items fragments, not nested fragments - user issue case', () => {
+            const document = parse(`
+                query FacetList($options: FacetListOptions, $facetValueListOptions: FacetValueListOptions) {
+                    facets(options: $options) {
+                        items {
+                            ...FacetWithValueList
+                        }
+                        totalItems
+                    }
+                }
+
+                fragment FacetWithValueList on Facet {
+                    id
+                    name
+                    isPrivate
+                    valueList(options: $facetValueListOptions) {
+                        totalItems
+                        items {
+                            ...FacetValue
+                        }
+                    }
+                }
+
+                fragment FacetValue on FacetValue {
+                    id
+                    name
+                    code
+                    translations {
+                        name
+                        languageCode
+                    }
+                }
+            `);
+
+            const result = includeOnlySelectedListFields(document, [{ name: 'name', isCustomField: false }]);
+
+            const resultQuery = normalizeQuery(print(result));
+
+            // Should include the top-level fragment
+            expect(resultQuery).toContain('...FacetWithValueList');
+            expect(resultQuery).toContain('fragment FacetWithValueList');
+
+            // Top-level fragment should be filtered (only name, no isPrivate)
+            expect(resultQuery).toContain(' name');
+            expect(resultQuery).not.toContain('isPrivate');
+            expect(resultQuery).not.toContain('valueList'); // This should be filtered out
+
+            // Should NOT include the nested FacetValue fragment since valueList was removed
+            expect(resultQuery).not.toContain('fragment FacetValue');
+            expect(resultQuery).not.toContain('...FacetValue');
+        });
+
+        it('should preserve nested fragments when their parent field is selected', () => {
+            const document = parse(`
+                query FacetList($options: FacetListOptions, $facetValueListOptions: FacetValueListOptions) {
+                    facets(options: $options) {
+                        items {
+                            ...FacetWithValueList
+                        }
+                        totalItems
+                    }
+                }
+
+                fragment FacetWithValueList on Facet {
+                    id
+                    name
+                    isPrivate
+                    valueList(options: $facetValueListOptions) {
+                        totalItems
+                        items {
+                            ...FacetValue
+                        }
+                    }
+                }
+
+                fragment FacetValue on FacetValue {
+                    id
+                    name
+                    code
+                    translations {
+                        name
+                        languageCode
+                    }
+                }
+            `);
+
+            const result = includeOnlySelectedListFields(document, [
+                { name: 'name', isCustomField: false },
+                { name: 'valueList', isCustomField: false },
+            ]);
+
+            const resultQuery = normalizeQuery(print(result));
+
+            // Should include the top-level fragment
+            expect(resultQuery).toContain('...FacetWithValueList');
+            expect(resultQuery).toContain('fragment FacetWithValueList');
+
+            // Top-level fragment should be filtered to include name and valueList
+            expect(resultQuery).toContain(' name');
+            expect(resultQuery).toContain('valueList');
+            expect(resultQuery).not.toContain('isPrivate');
+
+            // Should include the nested FacetValue fragment UNCHANGED since it's not a top-level items fragment
+            expect(resultQuery).toContain('fragment FacetValue');
+            expect(resultQuery).toContain('...FacetValue');
+            expect(resultQuery).toContain(' code'); // This should be preserved in the nested fragment
+            expect(resultQuery).toContain('translations'); // This should be preserved in the nested fragment
+        });
+    });
+
+    describe('unused fragment removal', () => {
+        it('should remove unused fragments when fields using them are filtered out', () => {
+            const document = parse(`
+                query ProductVariantList($options: ProductVariantListOptions) {
+                    productVariants(options: $options) {
+                        items {
+                            id
+                            price
+                            priceWithTax
+                            featuredAsset {
+                                ...Asset
+                            }
+                        }
+                        totalItems
+                    }
+                }
+
+                fragment Asset on Asset {
+                    id
+                    createdAt
+                    updatedAt
+                    name
+                    fileSize
+                    mimeType
+                    type
+                    preview
+                    source
+                    width
+                    height
+                    focalPoint {
+                        x
+                        y
+                    }
+                }
+            `);
+
+            // Select only price fields, excluding featuredAsset
+            const result = includeOnlySelectedListFields(document, [
+                { name: 'price', isCustomField: false },
+                { name: 'priceWithTax', isCustomField: false },
+            ]);
+
+            const resultQuery = normalizeQuery(print(result));
+
+            // Should include selected fields
+            expect(resultQuery).toContain('price');
+            expect(resultQuery).toContain('priceWithTax');
+
+            // Should exclude featuredAsset field
+            expect(resultQuery).not.toContain('featuredAsset');
+
+            // Should remove unused Asset fragment
+            expect(resultQuery).not.toContain('fragment Asset');
+            expect(resultQuery).not.toContain('...Asset');
+        });
+
+        it('should keep used fragments when fields using them are selected', () => {
+            const document = parse(`
+                query ProductVariantList($options: ProductVariantListOptions) {
+                    productVariants(options: $options) {
+                        items {
+                            id
+                            price
+                            featuredAsset {
+                                ...Asset
+                            }
+                        }
+                        totalItems
+                    }
+                }
+
+                fragment Asset on Asset {
+                    id
+                    name
+                    preview
+                    source
+                }
+            `);
+
+            // Select fields including featuredAsset
+            const result = includeOnlySelectedListFields(document, [
+                { name: 'price', isCustomField: false },
+                { name: 'featuredAsset', isCustomField: false },
+            ]);
+
+            const resultQuery = normalizeQuery(print(result));
+
+            // Should include selected fields
+            expect(resultQuery).toContain('price');
+            expect(resultQuery).toContain('featuredAsset');
+
+            // Should keep used Asset fragment
+            expect(resultQuery).toContain('fragment Asset');
+            expect(resultQuery).toContain('...Asset');
+        });
+
+        it('should handle nested fragment dependencies', () => {
+            const document = parse(`
+                query ProductList($options: ProductListOptions) {
+                    products(options: $options) {
+                        items {
+                            id
+                            name
+                            featuredAsset {
+                                ...AssetWithMetadata
+                            }
+                        }
+                        totalItems
+                    }
+                }
+
+                fragment AssetWithMetadata on Asset {
+                    ...AssetCore
+                    metadata {
+                        key
+                        value
+                    }
+                }
+
+                fragment AssetCore on Asset {
+                    id
+                    name
+                    preview
+                    source
+                }
+            `);
+
+            // Select only name, excluding featuredAsset
+            const result = includeOnlySelectedListFields(document, [{ name: 'name', isCustomField: false }]);
+
+            const resultQuery = normalizeQuery(print(result));
+
+            // Should include selected field
+            expect(resultQuery).toContain(' name');
+
+            // Should exclude featuredAsset field
+            expect(resultQuery).not.toContain('featuredAsset');
+
+            // Should remove all unused fragments
+            expect(resultQuery).not.toContain('fragment AssetWithMetadata');
+            expect(resultQuery).not.toContain('fragment AssetCore');
+            expect(resultQuery).not.toContain('...AssetWithMetadata');
+            expect(resultQuery).not.toContain('...AssetCore');
+        });
+
+        it('should keep transitive fragment dependencies when parent fragment is used', () => {
+            const document = parse(`
+                query ProductList($options: ProductListOptions) {
+                    products(options: $options) {
+                        items {
+                            id
+                            name
+                            featuredAsset {
+                                ...AssetWithMetadata
+                            }
+                        }
+                        totalItems
+                    }
+                }
+
+                fragment AssetWithMetadata on Asset {
+                    ...AssetCore
+                    metadata {
+                        key
+                        value
+                    }
+                }
+
+                fragment AssetCore on Asset {
+                    id
+                    name
+                    preview
+                    source
+                }
+            `);
+
+            // Select featuredAsset, which should keep all related fragments
+            const result = includeOnlySelectedListFields(document, [
+                { name: 'name', isCustomField: false },
+                { name: 'featuredAsset', isCustomField: false },
+            ]);
+
+            const resultQuery = normalizeQuery(print(result));
+
+            // Should include selected fields
+            expect(resultQuery).toContain(' name');
+            expect(resultQuery).toContain('featuredAsset');
+
+            // Should keep all used fragments
+            expect(resultQuery).toContain('fragment AssetWithMetadata');
+            expect(resultQuery).toContain('fragment AssetCore');
+            expect(resultQuery).toContain('...AssetWithMetadata');
+            expect(resultQuery).toContain('...AssetCore');
+        });
+
+        it('should handle mixed used and unused fragments', () => {
+            const document = parse(`
+                query ProductList($options: ProductListOptions) {
+                    products(options: $options) {
+                        items {
+                            id
+                            name
+                            featuredAsset {
+                                ...Asset
+                            }
+                            variants {
+                                ...ProductVariant
+                            }
+                        }
+                        totalItems
+                    }
+                }
+
+                fragment Asset on Asset {
+                    id
+                    name
+                    preview
+                }
+
+                fragment ProductVariant on ProductVariant {
+                    id
+                    name
+                    sku
+                    price
+                }
+
+                fragment UnusedFragment on Product {
+                    description
+                    slug
+                }
+            `);
+
+            // Select name and featuredAsset, excluding variants
+            const result = includeOnlySelectedListFields(document, [
+                { name: 'name', isCustomField: false },
+                { name: 'featuredAsset', isCustomField: false },
+            ]);
+
+            const resultQuery = normalizeQuery(print(result));
+
+            // Should include selected fields
+            expect(resultQuery).toContain(' name');
+            expect(resultQuery).toContain('featuredAsset');
+
+            // Should keep used Asset fragment
+            expect(resultQuery).toContain('fragment Asset');
+            expect(resultQuery).toContain('...Asset');
+
+            // Should exclude variants field
+            expect(resultQuery).not.toContain('variants');
+
+            // Should remove unused fragments
+            expect(resultQuery).not.toContain('fragment ProductVariant');
+            expect(resultQuery).not.toContain('fragment UnusedFragment');
+            expect(resultQuery).not.toContain('...ProductVariant');
+        });
+
+        it('should handle the exact case from user example', () => {
+            const document = parse(`
+                query ProductVariantList($options: ProductVariantListOptions) {
+                    productVariants(options: $options) {
+                        items {
+                            featuredAsset {
+                                ...Asset
+                            }
+                            price
+                            priceWithTax
+                            stockLevels {
+                                id
+                                stockOnHand
+                                stockAllocated
+                            }
+                        }
+                        totalItems
+                    }
+                }
+
+                fragment Asset on Asset {
+                    id
+                    createdAt
+                    updatedAt
+                    name
+                    fileSize
+                    mimeType
+                    type
+                    preview
+                    source
+                    width
+                    height
+                    focalPoint {
+                        x
+                        y
+                    }
+                }
+            `);
+
+            // Remove featuredAsset field as mentioned in the user's example
+            const result = includeOnlySelectedListFields(document, [
+                { name: 'price', isCustomField: false },
+                { name: 'priceWithTax', isCustomField: false },
+                { name: 'stockLevels', isCustomField: false },
+            ]);
+
+            const resultQuery = normalizeQuery(print(result));
+
+            // Should include selected fields
+            expect(resultQuery).toContain('price');
+            expect(resultQuery).toContain('priceWithTax');
+            expect(resultQuery).toContain('stockLevels');
+
+            // Should exclude featuredAsset
+            expect(resultQuery).not.toContain('featuredAsset');
+
+            // Should remove unused Asset fragment to prevent GraphQL error
+            expect(resultQuery).not.toContain('fragment Asset');
+            expect(resultQuery).not.toContain('...Asset');
+
+            // Verify the document is valid GraphQL by checking basic structure
+            expect(resultQuery).toContain('query ProductVariantList');
+            expect(resultQuery).toContain('productVariants');
+            expect(resultQuery).toContain('items');
+            expect(resultQuery).toContain('totalItems');
+        });
+    });
+
+    describe('unused variable removal', () => {
+        it('should remove unused variables when fields using them are filtered out - user issue case', () => {
+            const document = parse(`
+                query FacetList($options: FacetListOptions, $facetValueListOptions: FacetValueListOptions) {
+                    facets(options: $options) {
+                        items {
+                            ...FacetWithValueList
+                        }
+                        totalItems
+                    }
+                }
+
+                fragment FacetWithValueList on Facet {
+                    id
+                    name
+                    isPrivate
+                    valueList(options: $facetValueListOptions) {
+                        totalItems
+                        items {
+                            ...FacetValue
+                        }
+                    }
+                }
+
+                fragment FacetValue on FacetValue {
+                    name
+                }
+            `);
+
+            // Select only name, which should filter out valueList and its variable
+            const result = includeOnlySelectedListFields(document, [{ name: 'name', isCustomField: false }]);
+
+            const resultQuery = normalizeQuery(print(result));
+
+            // Should include selected field
+            expect(resultQuery).toContain(' name');
+
+            // Should exclude valueList field
+            expect(resultQuery).not.toContain('valueList');
+
+            // Should remove unused $facetValueListOptions variable
+            expect(resultQuery).not.toContain('$facetValueListOptions');
+            expect(resultQuery).not.toContain('FacetValueListOptions');
+
+            // Should keep used $options variable
+            expect(resultQuery).toContain('$options');
+            expect(resultQuery).toContain('FacetListOptions');
+
+            // Should remove unused FacetValue fragment
+            expect(resultQuery).not.toContain('fragment FacetValue');
+        });
+
+        it('should preserve variables that are still used', () => {
+            const document = parse(`
+                query FacetList($options: FacetListOptions, $facetValueListOptions: FacetValueListOptions) {
+                    facets(options: $options) {
+                        items {
+                            ...FacetWithValueList
+                        }
+                        totalItems
+                    }
+                }
+
+                fragment FacetWithValueList on Facet {
+                    id
+                    name
+                    valueList(options: $facetValueListOptions) {
+                        totalItems
+                        items {
+                            ...FacetValue
+                        }
+                    }
+                }
+
+                fragment FacetValue on FacetValue {
+                    name
+                }
+            `);
+
+            // Select name and valueList, which should preserve the variable
+            const result = includeOnlySelectedListFields(document, [
+                { name: 'name', isCustomField: false },
+                { name: 'valueList', isCustomField: false },
+            ]);
+
+            const resultQuery = normalizeQuery(print(result));
+
+            // Should include selected fields
+            expect(resultQuery).toContain(' name');
+            expect(resultQuery).toContain('valueList');
+
+            // Should keep both variables since both are used
+            expect(resultQuery).toContain('$options');
+            expect(resultQuery).toContain('$facetValueListOptions');
+            expect(resultQuery).toContain('FacetListOptions');
+            expect(resultQuery).toContain('FacetValueListOptions');
+
+            // Should keep FacetValue fragment since valueList is preserved
+            expect(resultQuery).toContain('fragment FacetValue');
+        });
+
+        it('should handle multiple variables with complex usage patterns', () => {
+            const document = parse(`
+                query ComplexQuery($listOptions: ListOptions, $searchTerm: String, $categoryId: ID, $priceRange: PriceRangeInput) {
+                    products(options: $listOptions) {
+                        items {
+                            id
+                            name
+                            searchResults(term: $searchTerm) {
+                                score
+                                highlight
+                            }
+                            category(id: $categoryId) {
+                                name
+                                path
+                            }
+                            variants(priceRange: $priceRange) {
+                                price
+                                priceWithTax
+                            }
+                        }
+                        totalItems
+                    }
+                }
+            `);
+
+            // Select only id and name, should remove most variables
+            const result = includeOnlySelectedListFields(document, [
+                { name: 'id', isCustomField: false },
+                { name: 'name', isCustomField: false },
+            ]);
+
+            const resultQuery = normalizeQuery(print(result));
+
+            // Should include selected fields
+            expect(resultQuery).toContain('id');
+            expect(resultQuery).toContain(' name');
+
+            // Should exclude fields that use variables
+            expect(resultQuery).not.toContain('searchResults');
+            expect(resultQuery).not.toContain('category');
+            expect(resultQuery).not.toContain('variants');
+
+            // Should keep only the used variable
+            expect(resultQuery).toContain('$listOptions');
+            expect(resultQuery).toContain('ListOptions');
+
+            // Should remove unused variables
+            expect(resultQuery).not.toContain('$searchTerm');
+            expect(resultQuery).not.toContain('$categoryId');
+            expect(resultQuery).not.toContain('$priceRange');
+            expect(resultQuery).not.toContain('String');
+            expect(resultQuery).not.toContain('ID');
+            expect(resultQuery).not.toContain('PriceRangeInput');
+        });
+
+        it('should handle variables in query-level arguments', () => {
+            const document = parse(`
+                query ProductSearch($options: ProductListOptions, $filters: ProductFilterInput) {
+                    products(options: $options, filters: $filters) {
+                        items {
+                            id
+                            name
+                            price
+                            category {
+                                name
+                            }
+                        }
+                        totalItems
+                    }
+                }
+            `);
+
+            // Select only name, which should remove fields but keep variables used at query level
+            const result = includeOnlySelectedListFields(document, [{ name: 'name', isCustomField: false }]);
+
+            const resultQuery = normalizeQuery(print(result));
+
+            // Should include selected field
+            expect(resultQuery).toContain(' name');
+
+            // Should exclude non-selected fields
+            expect(resultQuery).not.toContain('price');
+            expect(resultQuery).not.toContain('category');
+
+            // Should keep both variables since they're used at query level
+            expect(resultQuery).toContain('$options');
+            expect(resultQuery).toContain('$filters');
+            expect(resultQuery).toContain('ProductListOptions');
+            expect(resultQuery).toContain('ProductFilterInput');
+        });
+
+        it('should handle variables used in fragment arguments', () => {
+            const document = parse(`
+                query ProductList($options: ProductListOptions, $assetOptions: AssetListOptions) {
+                    products(options: $options) {
+                        items {
+                            ...ProductWithAssets
+                        }
+                        totalItems
+                    }
+                }
+
+                fragment ProductWithAssets on Product {
+                    id
+                    name
+                    assets(options: $assetOptions) {
+                        id
+                        preview
+                    }
+                }
+            `);
+
+            // Select only name, which should remove assets field and its variable
+            const result = includeOnlySelectedListFields(document, [{ name: 'name', isCustomField: false }]);
+
+            const resultQuery = normalizeQuery(print(result));
+
+            // Should include selected field
+            expect(resultQuery).toContain(' name');
+
+            // Should exclude assets field
+            expect(resultQuery).not.toContain('assets');
+
+            // Should keep used variable
+            expect(resultQuery).toContain('$options');
+            expect(resultQuery).toContain('ProductListOptions');
+
+            // Should remove unused variable
+            expect(resultQuery).not.toContain('$assetOptions');
+            expect(resultQuery).not.toContain('AssetListOptions');
+        });
+
+        it('should handle edge case with no variables', () => {
+            const document = parse(`
+                query ProductList {
+                    products {
+                        items {
+                            id
+                            name
+                            slug
+                        }
+                        totalItems
+                    }
+                }
+            `);
+
+            const result = includeOnlySelectedListFields(document, [{ name: 'name', isCustomField: false }]);
+
+            const resultQuery = normalizeQuery(print(result));
+
+            // Should include selected field
+            expect(resultQuery).toContain(' name');
+
+            // Should exclude non-selected fields
+            expect(resultQuery).not.toContain('slug');
+
+            // Query should remain valid with no variables
+            expect(resultQuery).toContain('query ProductList {');
+        });
+    });
+
+    describe('complex real-world scenarios', () => {
+        it('should handle a complex product list query', () => {
+            const document = parse(`
+                query GetProducts($options: ProductListOptions) {
+                    products(options: $options) {
+                        items {
+                            id
+                            createdAt
+                            updatedAt
+                            name
+                            slug
+                            enabled
+                            featuredAsset {
+                                id
+                                preview
+                                source
+                                width
+                                height
+                            }
+                            variantList {
+                                totalItems
+                                items {
+                                    id
+                                    name
+                                    sku
+                                }
+                            }
+                            customFields {
+                                shortDescription
+                                warrantyMonths
+                                isEcoFriendly
+                                featuredCollection {
+                                    id
+                                    name
+                                }
+                            }
+                        }
+                        totalItems
+                    }
+                }
+            `);
+
+            const result = includeOnlySelectedListFields(document, [
+                { name: 'id', isCustomField: false },
+                { name: 'name', isCustomField: false },
+                { name: 'featuredAsset', isCustomField: false },
+                { name: 'shortDescription', isCustomField: true },
+                { name: 'isEcoFriendly', isCustomField: true },
+            ]);
+
+            const resultQuery = normalizeQuery(print(result));
+
+            // Should include selected fields
+            expect(resultQuery).toContain('id');
+            expect(resultQuery).toContain(' name');
+            expect(resultQuery).toContain('featuredAsset');
+            expect(resultQuery).toContain('customFields { shortDescription isEcoFriendly }');
+
+            // Should exclude non-selected fields
+            expect(resultQuery).not.toContain('createdAt');
+            expect(resultQuery).not.toContain('updatedAt');
+            expect(resultQuery).not.toContain('slug');
+            expect(resultQuery).not.toContain('enabled');
+            expect(resultQuery).not.toContain('variantList');
+            expect(resultQuery).not.toContain('warrantyMonths');
+            expect(resultQuery).not.toContain('featuredCollection');
+
+            // Should preserve pagination
+            expect(resultQuery).toContain('totalItems');
+        });
+
+        it('should handle customer list with addresses', () => {
+            const document = parse(`
+                query CustomerList($options: CustomerListOptions) {
+                    customers(options: $options) {
+                        items {
+                            id
+                            title
+                            firstName
+                            lastName
+                            emailAddress
+                            phoneNumber
+                            addresses {
+                                id
+                                streetLine1
+                                streetLine2
+                                city
+                                province
+                                postalCode
+                                country {
+                                    id
+                                    code
+                                    name
+                                }
+                            }
+                            orders {
+                                totalItems
+                            }
+                        }
+                        totalItems
+                    }
+                }
+            `);
+
+            const result = includeOnlySelectedListFields(document, [
+                { name: 'id', isCustomField: false },
+                { name: 'emailAddress', isCustomField: false },
+                { name: 'firstName', isCustomField: false },
+                { name: 'lastName', isCustomField: false },
+            ]);
+
+            const resultQuery = normalizeQuery(print(result));
+
+            // Should include selected fields
+            expect(resultQuery).toContain('id');
+            expect(resultQuery).toContain('emailAddress');
+            expect(resultQuery).toContain('firstName');
+            expect(resultQuery).toContain('lastName');
+
+            // Should exclude non-selected fields
+            expect(resultQuery).not.toContain('title');
+            expect(resultQuery).not.toContain('phoneNumber');
+            expect(resultQuery).not.toContain('addresses');
+            expect(resultQuery).not.toContain('orders');
+        });
+    });
+
+    describe('memoization', () => {
+        it('should return the same reference for identical inputs', () => {
+            const document = parse(`
+                query ProductList($options: ProductListOptions) {
+                    products(options: $options) {
+                        items {
+                            id
+                            name
+                            slug
+                            enabled
+                        }
+                        totalItems
+                    }
+                }
+            `);
+
+            const selectedColumns = [
+                { name: 'id', isCustomField: false },
+                { name: 'name', isCustomField: false },
+            ];
+
+            const result1 = includeOnlySelectedListFields(document, selectedColumns);
+            const result2 = includeOnlySelectedListFields(document, selectedColumns);
+
+            // Should return the exact same reference from cache
+            expect(result1).toBe(result2);
+        });
+
+        it('should handle different column orders consistently', () => {
+            const document = parse(`
+                query ProductList($options: ProductListOptions) {
+                    products(options: $options) {
+                        items {
+                            id
+                            name
+                            slug
+                        }
+                        totalItems
+                    }
+                }
+            `);
+
+            const columns1 = [
+                { name: 'name', isCustomField: false },
+                { name: 'id', isCustomField: false },
+            ];
+
+            const columns2 = [
+                { name: 'id', isCustomField: false },
+                { name: 'name', isCustomField: false },
+            ];
+
+            const result1 = includeOnlySelectedListFields(document, columns1);
+            const result2 = includeOnlySelectedListFields(document, columns2);
+
+            // Should return the same result regardless of column order (due to sorting in cache key)
+            expect(result1).toBe(result2);
+        });
+    });
+
+    describe('column dependencies', () => {
+        it('should include dependency fields even when not explicitly selected', () => {
+            const document = parse(`
+                query CollectionList($options: CollectionListOptions) {
+                    collections(options: $options) {
+                        items {
+                            id
+                            name
+                            slug
+                            children {
+                                id
+                                name
+                            }
+                            breadcrumbs {
+                                id
+                                name
+                            }
+                            position
+                        }
+                        totalItems
+                    }
+                }
+            `);
+
+            // Select only 'name' but declare dependencies on 'children' and 'breadcrumbs'
+            const result = includeOnlySelectedListFields(document, [
+                {
+                    name: 'name',
+                    isCustomField: false,
+                    dependencies: ['children', 'breadcrumbs'],
+                },
+            ]);
+
+            const resultQuery = normalizeQuery(print(result));
+
+            // Should include selected field
+            expect(resultQuery).toContain('name');
+
+            // Should include dependency fields
+            expect(resultQuery).toContain('children');
+            expect(resultQuery).toContain('breadcrumbs');
+
+            // Should exclude non-selected, non-dependency fields
+            expect(resultQuery).not.toContain('slug');
+            expect(resultQuery).not.toContain('position');
+        });
+
+        it('should handle multiple columns with different dependencies', () => {
+            const document = parse(`
+                query ProductList($options: ProductListOptions) {
+                    products(options: $options) {
+                        items {
+                            id
+                            name
+                            slug
+                            enabled
+                            variants {
+                                id
+                                name
+                            }
+                            assets {
+                                id
+                                preview
+                            }
+                            featuredAsset {
+                                id
+                                preview
+                            }
+                        }
+                        totalItems
+                    }
+                }
+            `);
+
+            const result = includeOnlySelectedListFields(document, [
+                {
+                    name: 'name',
+                    isCustomField: false,
+                    dependencies: ['featuredAsset'], // For thumbnail display
+                },
+                {
+                    name: 'enabled',
+                    isCustomField: false,
+                    dependencies: ['variants'], // For variant count display
+                },
+            ]);
+
+            const resultQuery = normalizeQuery(print(result));
+
+            // Should include selected fields
+            expect(resultQuery).toContain('name');
+            expect(resultQuery).toContain('enabled');
+
+            // Should include dependency fields
+            expect(resultQuery).toContain('featuredAsset');
+            expect(resultQuery).toContain('variants');
+
+            // Should exclude non-selected, non-dependency fields
+            expect(resultQuery).not.toContain('slug');
+            expect(resultQuery).not.toContain('assets');
+        });
+
+        it('should cache correctly with dependencies', () => {
+            const document = parse(`
+                query TestList($options: TestListOptions) {
+                    tests(options: $options) {
+                        items {
+                            id
+                            name
+                            data
+                            metadata
+                        }
+                        totalItems
+                    }
+                }
+            `);
+
+            const columnsWithDeps = [
+                {
+                    name: 'name',
+                    isCustomField: false,
+                    dependencies: ['metadata'],
+                },
+            ];
+
+            const result1 = includeOnlySelectedListFields(document, columnsWithDeps);
+            const result2 = includeOnlySelectedListFields(document, columnsWithDeps);
+
+            // Should return the same reference from cache
+            expect(result1).toBe(result2);
+        });
+
+        it('should handle dependencies with custom fields', () => {
+            const document = parse(`
+                query ProductList($options: ProductListOptions) {
+                    products(options: $options) {
+                        items {
+                            id
+                            name
+                            customFields {
+                                shortDescription
+                                isEcoFriendly
+                                relatedProducts {
+                                    id
+                                    name
+                                }
+                            }
+                        }
+                        totalItems
+                    }
+                }
+            `);
+
+            const result = includeOnlySelectedListFields(document, [
+                {
+                    name: 'name',
+                    isCustomField: false,
+                    dependencies: ['customFields'], // Depends on custom fields for rendering logic
+                },
+                {
+                    name: 'shortDescription',
+                    isCustomField: true,
+                },
+            ]);
+
+            const resultQuery = normalizeQuery(print(result));
+
+            // Should include selected fields
+            expect(resultQuery).toContain('name');
+
+            // Should include customFields (as dependency and for custom field)
+            expect(resultQuery).toContain('customFields');
+            expect(resultQuery).toContain('shortDescription');
+
+            // Should include id for valid query
+            expect(resultQuery).toContain('id');
+        });
+    });
+});

+ 940 - 0
packages/dashboard/src/lib/framework/document-introspection/include-only-selected-list-fields.ts

@@ -0,0 +1,940 @@
+import {
+    ArgumentNode,
+    DocumentNode,
+    FieldNode,
+    FragmentDefinitionNode,
+    FragmentSpreadNode,
+    Kind,
+    SelectionNode,
+    VariableNode,
+    visit,
+} from 'graphql';
+
+import { getQueryName } from './get-document-structure.js';
+
+// Simple LRU-style cache for memoization
+const filterCache = new Map<string, DocumentNode>();
+const MAX_CACHE_SIZE = 100;
+
+// Fast document fingerprinting using WeakMap for reference tracking
+const documentIds = new WeakMap<DocumentNode, string>();
+let documentCounter = 0;
+
+/**
+ * Get a fast, stable ID for a document node
+ */
+function getDocumentId(document: DocumentNode): string {
+    let id = documentIds.get(document);
+    if (!id) {
+        // For new documents, create a lightweight structural hash
+        id = createDocumentFingerprint(document);
+        documentIds.set(document, id);
+    }
+    return id;
+}
+
+/**
+ * Create a lightweight fingerprint of document structure (much faster than print())
+ */
+function createDocumentFingerprint(document: DocumentNode): string {
+    const parts: string[] = [];
+
+    for (const def of document.definitions) {
+        if (def.kind === Kind.OPERATION_DEFINITION) {
+            parts.push(`op:${def.operation}:${def.name?.value || 'anon'}`);
+            // Just count selections, don't traverse them
+            parts.push(`sel:${def.selectionSet.selections.length}`);
+        } else if (def.kind === Kind.FRAGMENT_DEFINITION) {
+            parts.push(`frag:${def.name.value}:${def.typeCondition.name.value}`);
+            parts.push(`sel:${def.selectionSet.selections.length}`);
+        }
+    }
+
+    return `doc_${++documentCounter}_${parts.join('_')}`;
+}
+
+/**
+ * Create a stable cache key from document and selected columns
+ */
+function createCacheKey(
+    document: DocumentNode,
+    selectedColumns: Array<{ name: string; isCustomField: boolean; dependencies?: string[] }>,
+): string {
+    const docId = getDocumentId(document);
+    const columnsKey = sortJoin(
+        selectedColumns.map(col => {
+            const deps = col.dependencies ? sortJoin(col.dependencies, '+') : '';
+            const depsPart = deps ? `:deps(${deps})` : '';
+            return `${col.name}:${String(col.isCustomField)}${depsPart}`;
+        }),
+        ',',
+    );
+    return `${docId}|${columnsKey}`;
+}
+
+function sortJoin<T>(arr: T[], separator: string): string {
+    return arr.slice(0).sort().join(separator);
+}
+
+/**
+ * @description
+ * This function takes a list query document such as:
+ * ```gql
+ * query ProductList($options: ProductListOptions) {
+ *     products(options: $options) {
+ *         items {
+ *             id
+ *             createdAt
+ *             updatedAt
+ *             featuredAsset {
+ *                 id
+ *                 preview
+ *             }
+ *             name
+ *             slug
+ *             enabled
+ *         }
+ *         totalItems
+ *     }
+ * }
+ * ```
+ * and an array of selected columns, and returns a new document which only selects the
+ * specified columns. So if `selectedColumns` equals `[{ name: 'id', isCustomField: false }]`,
+ * then the resulting document's `items` fields would be `{ id }`.
+ *
+ * Columns can also declare dependencies on other fields that are required for rendering
+ * but not necessarily visible. For example:
+ * ```js
+ * selectedColumns = [{
+ *   name: 'name',
+ *   isCustomField: false,
+ *   dependencies: ['children', 'breadcrumbs'] // Always include these fields
+ * }]
+ * ```
+ * This ensures that cell renderers can safely access dependent fields even when they're
+ * not part of the visible column set.
+ *
+ * @param listQuery The GraphQL document to filter
+ * @param selectedColumns Array of column definitions with optional dependencies
+ */
+export function includeOnlySelectedListFields<T extends DocumentNode>(
+    listQuery: T,
+    selectedColumns: Array<{
+        name: string;
+        isCustomField: boolean;
+        dependencies?: string[];
+    }>,
+): T {
+    // If no columns selected, return the original document
+    if (selectedColumns.length === 0) {
+        return listQuery;
+    }
+
+    // Check cache first
+    const cacheKey = createCacheKey(listQuery, selectedColumns);
+    if (filterCache.has(cacheKey)) {
+        return filterCache.get(cacheKey) as T;
+    }
+
+    // Get the query name to identify the main list query field
+    const queryName = getQueryName(listQuery);
+
+    // Collect all required fields including dependencies
+    const allRequiredFields = new Set<string>();
+    const customFieldNames = new Set<string>();
+
+    selectedColumns.forEach(col => {
+        allRequiredFields.add(col.name);
+        if (col.isCustomField) {
+            customFieldNames.add(col.name);
+        }
+        // Add dependencies
+        col.dependencies?.forEach(dep => {
+            allRequiredFields.add(dep);
+            // Note: Dependencies are assumed to be regular fields unless they start with custom field patterns
+        });
+    });
+
+    const selectedFieldNames = allRequiredFields;
+
+    // Collect all fragments from the document
+    const fragments = collectFragments(listQuery);
+
+    // First pass: identify which fragments are directly used by the items field
+    const itemsFragments = getItemsFragments(listQuery, queryName);
+
+    // Visit and transform the document
+    const modifiedDocument = visit(listQuery, {
+        [Kind.FIELD]: {
+            enter(node: FieldNode, key, parent, path, ancestors): FieldNode | undefined {
+                // Check if we're at the query root field (e.g., "products")
+                const isQueryRoot =
+                    ancestors.some(
+                        ancestor =>
+                            ancestor &&
+                            typeof ancestor === 'object' &&
+                            'kind' in ancestor &&
+                            ancestor.kind === Kind.OPERATION_DEFINITION &&
+                            ancestor.operation === 'query',
+                    ) && node.name.value === queryName;
+
+                if (!isQueryRoot) {
+                    return undefined;
+                }
+
+                // Look for the "items" field within the query root
+                if (node.selectionSet) {
+                    const modifiedSelections = node.selectionSet.selections.map(selection => {
+                        if (isFieldWithName(selection, 'items')) {
+                            // Filter the items field to only include selected columns
+                            return filterItemsField(
+                                selection,
+                                selectedFieldNames,
+                                customFieldNames,
+                                fragments,
+                            );
+                        }
+                        return selection;
+                    });
+
+                    return {
+                        ...node,
+                        selectionSet: {
+                            ...node.selectionSet,
+                            selections: modifiedSelections,
+                        },
+                    };
+                }
+
+                return undefined;
+            },
+        },
+        [Kind.FRAGMENT_DEFINITION]: {
+            enter(node: FragmentDefinitionNode): FragmentDefinitionNode {
+                // Only filter fragments that are directly used by the items field
+                if (itemsFragments.has(node.name.value)) {
+                    return filterFragment(node, selectedFieldNames, customFieldNames, fragments);
+                }
+                // Leave other fragments untouched
+                return node;
+            },
+        },
+    });
+
+    // Remove unused fragments to prevent GraphQL validation errors
+    const withoutUnusedFragments = removeUnusedFragments(modifiedDocument);
+
+    // Remove unused variables to prevent GraphQL validation errors
+    const result = removeUnusedVariables(withoutUnusedFragments);
+
+    // Cache the result with LRU eviction
+    if (filterCache.size >= MAX_CACHE_SIZE) {
+        const firstKey = filterCache.keys().next().value;
+        if (firstKey) {
+            filterCache.delete(firstKey);
+        }
+    }
+    filterCache.set(cacheKey, result);
+    return result;
+}
+
+/**
+ * Collect all fragments from the document
+ */
+function collectFragments(document: DocumentNode): Record<string, FragmentDefinitionNode> {
+    const fragments: Record<string, FragmentDefinitionNode> = {};
+    for (const definition of document.definitions) {
+        if (definition.kind === Kind.FRAGMENT_DEFINITION) {
+            fragments[definition.name.value] = definition;
+        }
+    }
+    return fragments;
+}
+
+/**
+ * Check if a selection is a field with the given name
+ */
+function isFieldWithName(selection: SelectionNode, fieldName: string): selection is FieldNode {
+    return selection.kind === Kind.FIELD && selection.name.value === fieldName;
+}
+
+/**
+ * Check if a selection is a field with the given name and has a selection set
+ */
+function isFieldWithNameAndSelections(selection: SelectionNode, fieldName: string): selection is FieldNode {
+    return isFieldWithName(selection, fieldName) && !!selection.selectionSet;
+}
+
+/**
+ * Collect fragment spreads from a selection set
+ */
+function collectFragmentSpreads(selections: readonly SelectionNode[]): string[] {
+    const fragmentNames: string[] = [];
+    for (const selection of selections) {
+        if (selection.kind === Kind.FRAGMENT_SPREAD) {
+            fragmentNames.push(selection.name.value);
+        }
+    }
+    return fragmentNames;
+}
+
+/**
+ * Check if a selection is a field node
+ */
+function isField(selection: SelectionNode): selection is FieldNode {
+    return selection.kind === Kind.FIELD;
+}
+
+/**
+ * Find the items field within a query field's selections
+ */
+function findItemsFieldFragments(querySelections: readonly SelectionNode[]): string[] {
+    for (const selection of querySelections) {
+        if (isFieldWithNameAndSelections(selection, 'items') && selection.selectionSet) {
+            return collectFragmentSpreads(selection.selectionSet.selections);
+        }
+    }
+    return [];
+}
+
+/**
+ * Find the query field with the given name and process its items field
+ */
+function findQueryFieldFragments(selections: readonly SelectionNode[], queryName: string): string[] {
+    for (const selection of selections) {
+        if (isFieldWithNameAndSelections(selection, queryName) && selection.selectionSet) {
+            return findItemsFieldFragments(selection.selectionSet.selections);
+        }
+    }
+    return [];
+}
+
+/**
+ * Get fragments that are directly used by the items field (not nested fragments)
+ */
+function getItemsFragments(document: DocumentNode, queryName: string): Set<string> {
+    const itemsFragments = new Set<string>();
+
+    for (const definition of document.definitions) {
+        if (definition.kind === Kind.OPERATION_DEFINITION && definition.operation === 'query') {
+            const fragmentNames = findQueryFieldFragments(definition.selectionSet.selections, queryName);
+            fragmentNames.forEach(name => itemsFragments.add(name));
+        }
+    }
+
+    return itemsFragments;
+}
+
+/**
+ * Context for filtering field selections
+ */
+interface FieldSelectionContext {
+    itemsField: FieldNode;
+    availableFields: Map<string, SelectionNode>;
+    fragmentSpreads: Map<string, FragmentSpreadNode>;
+    fragments: Record<string, FragmentDefinitionNode>;
+    neededFragments: Set<string>;
+    filteredSelections: SelectionNode[];
+}
+
+/**
+ * Check if a field is selected directly in the items field (not from a fragment)
+ */
+function isDirectField(fieldName: string, itemsField: FieldNode): boolean {
+    if (!itemsField.selectionSet) return false;
+    return itemsField.selectionSet.selections.some(sel => isFieldWithName(sel, fieldName));
+}
+
+/**
+ * Add a regular field to filtered selections
+ */
+function addRegularField(fieldName: string, context: FieldSelectionContext): void {
+    if (!context.availableFields.has(fieldName)) return;
+
+    if (isDirectField(fieldName, context.itemsField)) {
+        const fieldSelection = context.availableFields.get(fieldName);
+        if (fieldSelection) {
+            context.filteredSelections.push(fieldSelection);
+        }
+    } else {
+        // Field comes from a fragment - mark fragments as needed
+        for (const [fragName] of context.fragmentSpreads) {
+            const fragment = context.fragments[fragName];
+            if (fragment && fragmentContainsField(fragment, fieldName, context.fragments)) {
+                context.neededFragments.add(fragName);
+            }
+        }
+    }
+}
+
+/**
+ * Add custom fields to filtered selections
+ */
+function addCustomFields(customFieldNames: Set<string>, context: FieldSelectionContext): void {
+    if (customFieldNames.size === 0 || !context.availableFields.has('customFields')) return;
+
+    if (isDirectField('customFields', context.itemsField)) {
+        const customFieldsSelection = context.availableFields.get('customFields');
+        if (customFieldsSelection && customFieldsSelection.kind === Kind.FIELD) {
+            const filteredCustomFields = filterCustomFields(customFieldsSelection, customFieldNames);
+            if (filteredCustomFields) {
+                context.filteredSelections.push(filteredCustomFields);
+            }
+        }
+    } else {
+        // customFields comes from a fragment
+        for (const [fragName] of context.fragmentSpreads) {
+            const fragment = context.fragments[fragName];
+            if (fragment && fragmentContainsField(fragment, 'customFields', context.fragments)) {
+                context.neededFragments.add(fragName);
+            }
+        }
+    }
+}
+
+/**
+ * Check if context has only __typename as a field (which doesn't count as valid content)
+ */
+function hasOnlyTypenameField(context: FieldSelectionContext): boolean {
+    return (
+        context.filteredSelections.length === 1 &&
+        isFieldWithName(context.filteredSelections[0], '__typename')
+    );
+}
+
+/**
+ * Check if context has valid fields that make the query meaningful
+ */
+function hasValidFields(context: FieldSelectionContext): boolean {
+    return context.filteredSelections.length > 0 && !hasOnlyTypenameField(context);
+}
+
+/**
+ * Add id field directly from available fields
+ */
+function addDirectIdField(context: FieldSelectionContext): void {
+    const idField = context.availableFields.get('id');
+    if (idField) {
+        context.filteredSelections.push(idField);
+    }
+}
+
+/**
+ * Add id field from a fragment that contains it
+ */
+function addIdFieldFromFragment(context: FieldSelectionContext): void {
+    for (const [fragName] of context.fragmentSpreads) {
+        const fragment = context.fragments[fragName];
+        if (fragment && fragmentContainsField(fragment, 'id', context.fragments)) {
+            const fragmentSpread = context.fragmentSpreads.get(fragName);
+            if (fragmentSpread) {
+                context.filteredSelections.push(fragmentSpread);
+            }
+            break;
+        }
+    }
+}
+
+/**
+ * Create a minimal id field when none exists
+ */
+function createMinimalIdField(context: FieldSelectionContext): void {
+    context.filteredSelections.push({
+        kind: Kind.FIELD,
+        name: { kind: Kind.NAME, value: 'id' },
+    });
+}
+
+/**
+ * Ensure at least id field is included to maintain valid query
+ */
+function ensureIdField(context: FieldSelectionContext): void {
+    if (hasValidFields(context)) return;
+
+    if (context.availableFields.has('id')) {
+        if (isDirectField('id', context.itemsField)) {
+            addDirectIdField(context);
+        } else {
+            addIdFieldFromFragment(context);
+        }
+    } else {
+        createMinimalIdField(context);
+    }
+}
+
+/**
+ * Initialize field selection context
+ */
+function initializeFieldSelectionContext(
+    itemsField: FieldNode,
+    fragments: Record<string, FragmentDefinitionNode>,
+): FieldSelectionContext {
+    return {
+        itemsField,
+        availableFields: new Map(),
+        fragmentSpreads: new Map(),
+        fragments,
+        neededFragments: new Set(),
+        filteredSelections: [],
+    };
+}
+
+/**
+ * Collect available fields and fragment spreads from selections
+ */
+function collectAvailableSelections(
+    context: FieldSelectionContext,
+    selections: readonly SelectionNode[],
+    fragments: Record<string, FragmentDefinitionNode>,
+): void {
+    for (const selection of selections) {
+        if (isField(selection)) {
+            context.availableFields.set(selection.name.value, selection);
+        } else if (selection.kind === Kind.FRAGMENT_SPREAD) {
+            context.fragmentSpreads.set(selection.name.value, selection);
+            const fragment = fragments[selection.name.value];
+            if (fragment) {
+                collectFieldsFromFragment(fragment, fragments, context.availableFields);
+            }
+        }
+    }
+}
+
+/**
+ * Add __typename field if it exists in original selections
+ */
+function addTypenameField(context: FieldSelectionContext): void {
+    if (context.availableFields.has('__typename')) {
+        const typenameField = context.availableFields.get('__typename');
+        if (typenameField) {
+            context.filteredSelections.push(typenameField);
+        }
+    }
+}
+
+/**
+ * Add all selected regular fields
+ */
+function addSelectedFields(context: FieldSelectionContext, selectedFieldNames: Set<string>): void {
+    for (const fieldName of selectedFieldNames) {
+        if (fieldName === '__typename') continue;
+        addRegularField(fieldName, context);
+    }
+}
+
+/**
+ * Add needed fragment spreads to filtered selections
+ */
+function addNeededFragmentSpreads(context: FieldSelectionContext): void {
+    for (const fragName of context.neededFragments) {
+        const fragmentSpread = context.fragmentSpreads.get(fragName);
+        if (fragmentSpread) {
+            context.filteredSelections.push(fragmentSpread);
+        }
+    }
+}
+
+/**
+ * Add inline fragments from original selections
+ */
+function addInlineFragments(context: FieldSelectionContext, selections: readonly SelectionNode[]): void {
+    for (const selection of selections) {
+        if (selection.kind === Kind.INLINE_FRAGMENT) {
+            context.filteredSelections.push(selection);
+        }
+    }
+}
+
+/**
+ * Filter the items field to only include selected columns
+ */
+function filterItemsField(
+    itemsField: FieldNode,
+    selectedFieldNames: Set<string>,
+    customFieldNames: Set<string>,
+    fragments: Record<string, FragmentDefinitionNode>,
+): FieldNode {
+    if (!itemsField.selectionSet) {
+        return itemsField;
+    }
+
+    const context = initializeFieldSelectionContext(itemsField, fragments);
+
+    collectAvailableSelections(context, itemsField.selectionSet.selections, fragments);
+    addTypenameField(context);
+    addSelectedFields(context, selectedFieldNames);
+    addCustomFields(customFieldNames, context);
+    addNeededFragmentSpreads(context);
+    addInlineFragments(context, itemsField.selectionSet.selections);
+    ensureIdField(context);
+
+    return {
+        ...itemsField,
+        selectionSet: {
+            ...itemsField.selectionSet,
+            selections: context.filteredSelections,
+        },
+    };
+}
+
+/**
+ * Collect all fields from a fragment recursively
+ */
+function collectFieldsFromFragment(
+    fragment: FragmentDefinitionNode,
+    fragments: Record<string, FragmentDefinitionNode>,
+    availableFields: Map<string, SelectionNode>,
+): void {
+    for (const selection of fragment.selectionSet.selections) {
+        if (isField(selection)) {
+            availableFields.set(selection.name.value, selection);
+        } else if (selection.kind === Kind.FRAGMENT_SPREAD) {
+            const nestedFragment = fragments[selection.name.value];
+            if (nestedFragment) {
+                collectFieldsFromFragment(nestedFragment, fragments, availableFields);
+            }
+        }
+    }
+}
+
+/**
+ * Check if a fragment contains a specific field
+ */
+function fragmentContainsField(
+    fragment: FragmentDefinitionNode,
+    fieldName: string,
+    fragments: Record<string, FragmentDefinitionNode>,
+): boolean {
+    for (const selection of fragment.selectionSet.selections) {
+        if (isFieldWithName(selection, fieldName)) {
+            return true;
+        } else if (selection.kind === Kind.FRAGMENT_SPREAD) {
+            const nestedFragment = fragments[selection.name.value];
+            if (nestedFragment && fragmentContainsField(nestedFragment, fieldName, fragments)) {
+                return true;
+            }
+        }
+    }
+    return false;
+}
+
+/**
+ * Process a field selection for fragment filtering
+ */
+function processFragmentFieldSelection(
+    selection: FieldNode,
+    selectedFieldNames: Set<string>,
+    customFieldNames: Set<string>,
+    filteredSelections: SelectionNode[],
+): void {
+    const fieldName = selection.name.value;
+
+    if (fieldName === '__typename') {
+        filteredSelections.push(selection);
+    } else if (selectedFieldNames.has(fieldName)) {
+        filteredSelections.push(selection);
+    } else if (fieldName === 'customFields' && customFieldNames.size > 0) {
+        const filteredCustomFields = filterCustomFields(selection, customFieldNames);
+        if (filteredCustomFields) {
+            filteredSelections.push(filteredCustomFields);
+        }
+    }
+}
+
+/**
+ * Check if a fragment spread contains any selected fields
+ */
+function fragmentSpreadContainsSelectedFields(
+    spreadFragment: FragmentDefinitionNode,
+    selectedFieldNames: Set<string>,
+    fragments: Record<string, FragmentDefinitionNode>,
+): boolean {
+    for (const fieldName of selectedFieldNames) {
+        if (fragmentContainsField(spreadFragment, fieldName, fragments)) {
+            return true;
+        }
+    }
+    return false;
+}
+
+/**
+ * Process a fragment spread selection for fragment filtering
+ */
+function processFragmentSpreadSelection(
+    selection: FragmentSpreadNode,
+    selectedFieldNames: Set<string>,
+    fragments: Record<string, FragmentDefinitionNode>,
+    filteredSelections: SelectionNode[],
+): void {
+    const spreadFragment = fragments[selection.name.value];
+    if (
+        spreadFragment &&
+        fragmentSpreadContainsSelectedFields(spreadFragment, selectedFieldNames, fragments)
+    ) {
+        filteredSelections.push(selection);
+    }
+}
+
+/**
+ * Process all selections in a fragment
+ */
+function processFragmentSelections(
+    selections: readonly SelectionNode[],
+    selectedFieldNames: Set<string>,
+    customFieldNames: Set<string>,
+    fragments: Record<string, FragmentDefinitionNode>,
+): SelectionNode[] {
+    const filteredSelections: SelectionNode[] = [];
+
+    for (const selection of selections) {
+        if (isField(selection)) {
+            processFragmentFieldSelection(
+                selection,
+                selectedFieldNames,
+                customFieldNames,
+                filteredSelections,
+            );
+        } else if (selection.kind === Kind.FRAGMENT_SPREAD) {
+            processFragmentSpreadSelection(selection, selectedFieldNames, fragments, filteredSelections);
+        } else if (selection.kind === Kind.INLINE_FRAGMENT) {
+            // Keep inline fragments for now - more complex filtering would need type info
+            filteredSelections.push(selection);
+        }
+    }
+
+    return filteredSelections;
+}
+
+/**
+ * Ensure fragment has at least one field by adding id if available
+ */
+function ensureFragmentHasFields(
+    filteredSelections: SelectionNode[],
+    originalSelections: readonly SelectionNode[],
+): void {
+    if (filteredSelections.length === 0) {
+        // Add id if it exists in the original fragment
+        for (const selection of originalSelections) {
+            if (isFieldWithName(selection, 'id')) {
+                filteredSelections.push(selection);
+                break;
+            }
+        }
+    }
+}
+
+/**
+ * Filter a fragment to only include selected fields
+ */
+function filterFragment(
+    fragment: FragmentDefinitionNode,
+    selectedFieldNames: Set<string>,
+    customFieldNames: Set<string>,
+    fragments: Record<string, FragmentDefinitionNode>,
+): FragmentDefinitionNode {
+    const filteredSelections = processFragmentSelections(
+        fragment.selectionSet.selections,
+        selectedFieldNames,
+        customFieldNames,
+        fragments,
+    );
+
+    ensureFragmentHasFields(filteredSelections, fragment.selectionSet.selections);
+
+    return {
+        ...fragment,
+        selectionSet: {
+            ...fragment.selectionSet,
+            selections: filteredSelections,
+        },
+    };
+}
+
+/**
+ * Filter the customFields selection to only include selected custom fields
+ */
+function filterCustomFields(customFieldsNode: FieldNode, customFieldNames: Set<string>): FieldNode | null {
+    if (!customFieldsNode.selectionSet) {
+        return customFieldsNode;
+    }
+
+    const filteredSelections = customFieldsNode.selectionSet.selections.filter(selection => {
+        if (isField(selection)) {
+            return customFieldNames.has(selection.name.value);
+        }
+        // Keep fragments as they might contain selected custom fields
+        return true;
+    });
+
+    if (filteredSelections.length === 0) {
+        return null;
+    }
+
+    return {
+        ...customFieldsNode,
+        selectionSet: {
+            ...customFieldsNode.selectionSet,
+            selections: filteredSelections,
+        },
+    };
+}
+
+/**
+ * Remove unused fragments from the document to prevent GraphQL validation errors
+ */
+function removeUnusedFragments<T extends DocumentNode>(document: T): T {
+    // First, collect all fragment names that are actually used in the document
+    const usedFragments = new Set<string>();
+
+    // Helper function to recursively find fragment spreads
+    const findFragmentSpreads = (selections: readonly SelectionNode[]) => {
+        for (const selection of selections) {
+            if (selection.kind === Kind.FRAGMENT_SPREAD) {
+                usedFragments.add(selection.name.value);
+            } else if (selection.kind === Kind.INLINE_FRAGMENT && selection.selectionSet) {
+                findFragmentSpreads(selection.selectionSet.selections);
+            } else if (selection.kind === Kind.FIELD && selection.selectionSet) {
+                findFragmentSpreads(selection.selectionSet.selections);
+            }
+        }
+    };
+
+    // Look through all operations to find used fragments
+    for (const definition of document.definitions) {
+        if (definition.kind === Kind.OPERATION_DEFINITION) {
+            findFragmentSpreads(definition.selectionSet.selections);
+        }
+    }
+
+    // Now we need to handle transitive dependencies - fragments that use other fragments
+    let foundNewFragments = true;
+    while (foundNewFragments) {
+        foundNewFragments = false;
+        for (const definition of document.definitions) {
+            if (definition.kind === Kind.FRAGMENT_DEFINITION && usedFragments.has(definition.name.value)) {
+                const previousSize = usedFragments.size;
+                findFragmentSpreads(definition.selectionSet.selections);
+                if (usedFragments.size > previousSize) {
+                    foundNewFragments = true;
+                }
+            }
+        }
+    }
+
+    // Filter out unused fragment definitions
+    const filteredDefinitions = document.definitions.filter(definition => {
+        if (definition.kind === Kind.FRAGMENT_DEFINITION) {
+            return usedFragments.has(definition.name.value);
+        }
+        return true;
+    });
+
+    return {
+        ...document,
+        definitions: filteredDefinitions,
+    } as T;
+}
+
+/**
+ * Remove unused variables from the document to prevent GraphQL validation errors
+ */
+function removeUnusedVariables<T extends DocumentNode>(document: T): T {
+    const collector = new VariableUsageCollector();
+    const usedVariables = collector.collectFromDocument(document);
+
+    // Filter out unused variable definitions from operations
+    const modifiedDefinitions = document.definitions.map(definition => {
+        if (definition.kind === Kind.OPERATION_DEFINITION && definition.variableDefinitions) {
+            const filteredVariableDefinitions = definition.variableDefinitions.filter(variableDef =>
+                usedVariables.has(variableDef.variable.name.value),
+            );
+
+            return {
+                ...definition,
+                variableDefinitions: filteredVariableDefinitions,
+            };
+        }
+        return definition;
+    });
+
+    return {
+        ...document,
+        definitions: modifiedDefinitions,
+    } as T;
+}
+
+/**
+ * Variable usage collector that traverses GraphQL structures
+ */
+class VariableUsageCollector {
+    private readonly usedVariables = new Set<string>();
+
+    /**
+     * Collect variables from a GraphQL value (recursive)
+     */
+    private collectFromValue(value: any): void {
+        switch (value.kind) {
+            case Kind.VARIABLE:
+                this.usedVariables.add((value as VariableNode).name.value);
+                break;
+            case Kind.LIST:
+                value.values.forEach((item: any) => this.collectFromValue(item));
+                break;
+            case Kind.OBJECT:
+                value.fields.forEach((field: any) => this.collectFromValue(field.value));
+                break;
+            // For other value types (STRING, INT, FLOAT, BOOLEAN, NULL, ENUM), no variables to collect
+        }
+    }
+
+    /**
+     * Collect variables from field arguments
+     */
+    private collectFromArguments(args: readonly ArgumentNode[]): void {
+        args.forEach(arg => this.collectFromValue(arg.value));
+    }
+
+    /**
+     * Collect variables from selection set (recursive)
+     */
+    private collectFromSelections(selections: readonly SelectionNode[]): void {
+        selections.forEach(selection => {
+            switch (selection.kind) {
+                case Kind.FIELD:
+                    if (selection.arguments) {
+                        this.collectFromArguments(selection.arguments);
+                    }
+                    if (selection.selectionSet) {
+                        this.collectFromSelections(selection.selectionSet.selections);
+                    }
+                    break;
+                case Kind.INLINE_FRAGMENT:
+                    if (selection.selectionSet) {
+                        this.collectFromSelections(selection.selectionSet.selections);
+                    }
+                    break;
+                case Kind.FRAGMENT_SPREAD:
+                    // Fragment spreads are handled when processing fragment definitions
+                    break;
+            }
+        });
+    }
+
+    /**
+     * Collect all used variables from a document
+     */
+    collectFromDocument(document: DocumentNode): Set<string> {
+        this.usedVariables.clear();
+
+        document.definitions.forEach(definition => {
+            if (
+                definition.kind === Kind.OPERATION_DEFINITION ||
+                definition.kind === Kind.FRAGMENT_DEFINITION
+            ) {
+                this.collectFromSelections(definition.selectionSet.selections);
+            }
+        });
+
+        return new Set(this.usedVariables);
+    }
+}

+ 161 - 0
packages/dashboard/src/lib/framework/document-introspection/testing-utils.ts

@@ -0,0 +1,161 @@
+export function getMockSchemaInfo() {
+    return {
+        schemaInfo: {
+            types: {
+                Query: {
+                    products: ['ProductList', false, false, true],
+                    product: ['Product', false, false, false],
+                    collection: ['Collection', false, false, false],
+                    order: ['Order', false, false, false],
+                },
+                Mutation: {
+                    updateProduct: ['Product', false, false, false],
+                    adjustDraftOrderLine: ['Order', false, false, false],
+                },
+
+                Collection: {
+                    id: ['ID', false, false, false],
+                    name: ['String', false, false, false],
+                    productVariants: ['ProductVariantList', false, false, true],
+                },
+
+                ProductVariantList: {
+                    items: ['ProductVariant', false, true, false],
+                    totalItems: ['Int', false, false, false],
+                },
+
+                Product: {
+                    channels: ['Channel', false, true, false],
+                    id: ['ID', false, false, false],
+                    createdAt: ['DateTime', false, false, false],
+                    updatedAt: ['DateTime', false, false, false],
+                    languageCode: ['LanguageCode', false, false, false],
+                    name: ['String', false, false, false],
+                    slug: ['String', false, false, false],
+                    description: ['String', false, false, false],
+                    enabled: ['Boolean', false, false, false],
+                    featuredAsset: ['Asset', true, false, false],
+                    assets: ['Asset', false, true, false],
+                    variants: ['ProductVariant', false, true, false],
+                    variantList: ['ProductVariantList', false, false, true],
+                    optionGroups: ['ProductOptionGroup', false, true, false],
+                    facetValues: ['FacetValue', false, true, false],
+                    translations: ['ProductTranslation', false, true, false],
+                    collections: ['Collection', false, true, false],
+                    reviews: ['ProductReviewList', false, false, true],
+                    reviewsHistogram: ['ProductReviewHistogramItem', false, true, false],
+                    customFields: ['ProductCustomFields', true, false, false],
+                },
+                ProductVariantPrice: {
+                    currencyCode: ['CurrencyCode', false, false, false],
+                    price: ['Money', false, false, false],
+                    customFields: ['JSON', true, false, false],
+                },
+                ProductVariant: {
+                    enabled: ['Boolean', false, false, false],
+                    trackInventory: ['GlobalFlag', false, false, false],
+                    stockOnHand: ['Int', false, false, false],
+                    stockAllocated: ['Int', false, false, false],
+                    outOfStockThreshold: ['Int', false, false, false],
+                    useGlobalOutOfStockThreshold: ['Boolean', false, false, false],
+                    prices: ['ProductVariantPrice', false, true, false],
+                    stockLevels: ['StockLevel', false, true, false],
+                    stockMovements: ['StockMovementList', false, false, false],
+                    channels: ['Channel', false, true, false],
+                    id: ['ID', false, false, false],
+                    product: ['Product', false, false, false],
+                    productId: ['ID', false, false, false],
+                    createdAt: ['DateTime', false, false, false],
+                    updatedAt: ['DateTime', false, false, false],
+                    languageCode: ['LanguageCode', false, false, false],
+                    sku: ['String', false, false, false],
+                    name: ['String', false, false, false],
+                    featuredAsset: ['Asset', true, false, false],
+                    assets: ['Asset', false, true, false],
+                    price: ['Money', false, false, false],
+                    currencyCode: ['CurrencyCode', false, false, false],
+                    priceWithTax: ['Money', false, false, false],
+                    stockLevel: ['String', false, false, false],
+                    taxRateApplied: ['TaxRate', false, false, false],
+                    taxCategory: ['TaxCategory', false, false, false],
+                    options: ['ProductOption', false, true, false],
+                    facetValues: ['FacetValue', false, true, false],
+                    translations: ['ProductVariantTranslation', false, true, false],
+                    customFields: ['JSON', true, false, false],
+                },
+                ProductCustomFields: {
+                    custom1: ['String', false, false, false],
+                },
+
+                Asset: {
+                    id: ['ID', false, false, false],
+                    createdAt: ['DateTime', false, false, false],
+                    updatedAt: ['DateTime', false, false, false],
+                    name: ['String', false, false, false],
+                    type: ['AssetType', false, false, false],
+                    fileSize: ['Int', false, false, false],
+                    mimeType: ['String', false, false, false],
+                    width: ['Int', false, false, false],
+                    height: ['Int', false, false, false],
+                    source: ['String', false, false, false],
+                    preview: ['String', false, false, false],
+                    focalPoint: ['Coordinate', true, false, false],
+                    tags: ['Tag', false, true, false],
+                    customFields: ['JSON', true, false, false],
+                },
+                ProductTranslation: {
+                    id: ['ID', false, false, false],
+                    createdAt: ['DateTime', false, false, false],
+                    updatedAt: ['DateTime', false, false, false],
+                    languageCode: ['LanguageCode', false, false, false],
+                    name: ['String', false, false, false],
+                    slug: ['String', false, false, false],
+                    description: ['String', false, false, false],
+                    customFields: ['ProductTranslationCustomFields', true, false, false],
+                },
+                ProductList: {
+                    items: ['Product', false, true, false],
+                    totalItems: ['Int', false, false, false],
+                },
+
+                ProductVariantTranslation: {
+                    id: ['ID', false, false, false],
+                    createdAt: ['DateTime', false, false, false],
+                    updatedAt: ['DateTime', false, false, false],
+                    languageCode: ['LanguageCode', false, false, false],
+                    name: ['String', false, false, false],
+                },
+                Order: {
+                    id: ['ID', false, false, false],
+                    lines: ['OrderLine', false, true, false],
+                },
+                OrderLine: {
+                    id: ['ID', false, false, false],
+                    quantity: ['Int', false, false, false],
+                },
+            },
+            inputs: {
+                UpdateProductInput: {
+                    id: ['ID', false, false, false],
+                    name: ['String', false, false, false],
+                },
+                AdjustDraftOrderLineInput: {
+                    orderLineId: ['ID', false, false, false],
+                    quantity: ['Int', false, false, false],
+                },
+            },
+            scalars: [
+                'ID',
+                'String',
+                'Int',
+                'Boolean',
+                'Float',
+                'JSON',
+                'DateTime',
+                'Upload',
+                'CurrencyCode',
+            ],
+            enums: {},
+        },
+    };
+}

+ 54 - 4
packages/dashboard/src/lib/framework/page/list-page.tsx

@@ -17,7 +17,6 @@ import { ColumnFiltersState, SortingState, Table } from '@tanstack/react-table';
 import { TableOptions } from '@tanstack/table-core';
 
 import { BulkAction } from '@/vdb/framework/extension-api/types/index.js';
-import { addCustomFields } from '../document-introspection/add-custom-fields.js';
 import { FullWidthPageBlock, Page, PageActionBar, PageLayout, PageTitle } from '../layout-engine/page-layout.js';
 
 /**
@@ -48,6 +47,59 @@ export interface ListPageProps<
     deleteMutation?: TypedDocumentNode<any, { id: string }>;
     transformVariables?: (variables: V) => V;
     onSearchTermChange?: (searchTerm: string) => NonNullable<V['options']>['filter'];
+    /**
+     * @description
+     * Allows you to customize the rendering and other aspects of individual columns.
+     *
+     * By default, an appropriate component will be chosen to render the column data
+     * based on the data type of the field. However, in many cases you want to have
+     * more control over how the column data is rendered.
+     *
+     * @example
+     * ```tsx
+     * <ListPage
+     *   pageId="collection-list"
+     *   listQuery={collectionListDocument}
+     *   customizeColumns={{
+     *     // The key "name" matches one of the top-level fields of the
+     *     // list query type (Collection, in this example)
+     *     name: {
+     *       meta: {
+     *           // The Dashboard optimizes the list query `collectionListDocument` to
+     *           // only select field that are actually visible in the ListPage table.
+     *           // However, sometimes you want to render data from other fields, i.e.
+     *           // this column has a data dependency on the "children" and "breadcrumbs"
+     *           // fields in order to correctly render the "name" field.
+     *           // In this case, we can declare those data dependencies which means whenever
+     *           // the "name" column is visible, it will ensure the "children" and "breadcrumbs"
+     *           // fields are also selected in the query.
+     *           dependencies: ['children', 'breadcrumbs'],
+     *       },
+     *       header: 'Collection Name',
+     *       cell: ({ row }) => {
+     *         const isExpanded = row.getIsExpanded();
+     *         const hasChildren = !!row.original.children?.length;
+     *         return (
+     *           <div
+     *             style={{ marginLeft: (row.original.breadcrumbs?.length - 2) * 20 + 'px' }}
+     *             className="flex gap-2 items-center"
+     *           >
+     *             <Button
+     *               size="icon"
+     *               variant="secondary"
+     *               onClick={row.getToggleExpandedHandler()}
+     *               disabled={!hasChildren}
+     *               className={!hasChildren ? 'opacity-20' : ''}
+     *             >
+     *               {isExpanded ? <FolderOpen /> : <Folder />}
+     *             </Button>
+     *             <DetailPageButton id={row.original.id} label={row.original.name} />
+     *           </div>
+     *           );
+     *       },
+     *     },
+     * ```
+     */
     customizeColumns?: CustomizeColumnConfig<T>;
     additionalColumns?: AC;
     defaultColumnOrder?: (keyof ListQueryFields<T> | keyof AC | CustomFieldKeysOfItem<ListQueryFields<T>>)[];
@@ -242,8 +294,6 @@ export function ListPage<
         });
     }
 
-    const listQueryWithCustomFields = addCustomFields(listQuery);
-
     return (
         <Page pageId={pageId}>
             <PageTitle>{title}</PageTitle>
@@ -251,7 +301,7 @@ export function ListPage<
             <PageLayout>
                 <FullWidthPageBlock blockId="list-table">
                     <PaginatedListDataTable
-                        listQuery={listQueryWithCustomFields}
+                        listQuery={listQuery}
                         deleteMutation={deleteMutation}
                         transformVariables={transformVariables}
                         customizeColumns={customizeColumns as any}

+ 5 - 0
packages/dashboard/src/lib/hooks/use-extended-list-query.ts

@@ -8,6 +8,11 @@ import { toast } from 'sonner';
 import { usePageBlock } from './use-page-block.js';
 import { usePage } from './use-page.js';
 
+/**
+ * @description
+ * Extends the given list query document with additional fields that can be
+ * supplied via the Dashboard Extension API.
+ */
 export function useExtendedListQuery<T extends DocumentNode>(listQuery: T) {
     const { pageId } = usePage();
     const { blockId } = usePageBlock() ?? {};