Browse Source

feat(dashboard): Implement extension of list query documents

Michael Bromley 6 months ago
parent
commit
5190e4f5ab

+ 1 - 1
packages/dashboard/src/app/routes/_authenticated/_product-variants/product-variants.graphql.ts

@@ -3,7 +3,7 @@ import { graphql } from '@/graphql/graphql.js';
 
 export const productVariantListDocument = graphql(
     `
-        query ProductVariantLis($options: ProductVariantListOptions) {
+        query ProductVariantList($options: ProductVariantListOptions) {
             productVariants(options: $options) {
                 items {
                     id

+ 6 - 4
packages/dashboard/src/lib/components/shared/paginated-list-data-table.tsx

@@ -30,6 +30,7 @@ import {
 import { DisplayComponent } from '@/framework/component-registry/dynamic-component.js';
 import { BulkAction } from '@/framework/data-table/data-table-types.js';
 import { ResultOf } from '@/graphql/graphql.js';
+import { useExtendedListQuery } from '@/hooks/use-extended-list-query.js';
 import { Trans, useLingui } from '@/lib/trans.js';
 import { TypedDocumentNode } from '@graphql-typed-document-node/core';
 import {
@@ -276,6 +277,7 @@ export function PaginatedListDataTable<
     const [searchTerm, setSearchTerm] = React.useState<string>('');
     const debouncedSearchTerm = useDebounce(searchTerm, 500);
     const queryClient = useQueryClient();
+    const extendedListQuery = useExtendedListQuery(listQuery);
 
     const sort = sorting?.reduce((acc: any, sort: ColumnSort) => {
         const direction = sort.desc ? 'DESC' : 'ASC';
@@ -300,7 +302,7 @@ export function PaginatedListDataTable<
 
     const defaultQueryKey = [
         PaginatedListDataTableKey,
-        listQuery,
+        extendedListQuery,
         page,
         itemsPerPage,
         sorting,
@@ -329,14 +331,14 @@ export function PaginatedListDataTable<
             } as V;
 
             const transformedVariables = transformVariables ? transformVariables(variables) : variables;
-            return api.query(listQuery, transformedVariables);
+            return api.query(extendedListQuery, transformedVariables);
         },
         queryKey,
         placeholderData: keepPreviousData,
     });
 
-    const fields = useListQueryFields(listQuery);
-    const paginatedListObjectPath = getObjectPathToPaginatedList(listQuery);
+    const fields = useListQueryFields(extendedListQuery);
+    const paginatedListObjectPath = getObjectPathToPaginatedList(extendedListQuery);
 
     let listData = data as any;
     for (const path of paginatedListObjectPath) {

+ 14 - 0
packages/dashboard/src/lib/framework/data-table/data-table-extensions.ts

@@ -1,8 +1,10 @@
 import { BulkAction } from '@/framework/data-table/data-table-types.js';
+import { DocumentNode } from 'graphql';
 
 import { globalRegistry } from '../registry/global-registry.js';
 
 globalRegistry.register('bulkActionsRegistry', new Map<string, BulkAction[]>());
+globalRegistry.register('listQueryDocumentRegistry', new Map<string, DocumentNode[]>());
 
 export function getBulkActions(pageId: string, blockId = 'list-table'): BulkAction[] {
     const key = createKey(pageId, blockId);
@@ -16,6 +18,18 @@ export function addBulkAction(pageId: string, blockId: string | undefined, actio
     bulkActionsRegistry.set(key, [...existingActions, action]);
 }
 
+export function getListQueryDocuments(pageId: string, blockId = 'list-table'): DocumentNode[] {
+    const key = createKey(pageId, blockId);
+    return globalRegistry.get('listQueryDocumentRegistry').get(key) || [];
+}
+
+export function addListQueryDocument(pageId: string, blockId: string | undefined, document: DocumentNode) {
+    const listQueryDocumentRegistry = globalRegistry.get('listQueryDocumentRegistry');
+    const key = createKey(pageId, blockId);
+    const existingDocuments = listQueryDocumentRegistry.get(key) || [];
+    listQueryDocumentRegistry.set(key, [...existingDocuments, document]);
+}
+
 function createKey(pageId: string, blockId: string | undefined): string {
     return `${pageId}__${blockId ?? 'list-table'}`;
 }

+ 549 - 0
packages/dashboard/src/lib/framework/document-extension/extend-document.spec.ts

@@ -0,0 +1,549 @@
+import { graphql } from '@/graphql/graphql.js';
+import { print } from 'graphql';
+import { describe, expect, it } from 'vitest';
+
+import { extendDocument, gqlExtend } from './extend-document.js';
+
+/**
+ * Helper to strip indentation and normalize GraphQL SDL for comparison.
+ * Allows the expected result to be indented naturally in the code.
+ */
+function expectedSDL(str: string): string {
+    const lines = str.split('\n');
+    // Find the minimum indentation (excluding empty lines)
+    let minIndent = Infinity;
+    for (const line of lines) {
+        if (line.trim() === '') continue;
+        const indent = line.match(/^\s*/)?.[0].length || 0;
+        minIndent = Math.min(minIndent, indent);
+    }
+    // Remove the minimum indentation from all lines and normalize
+    return lines
+        .map(line => line.slice(minIndent).trim())
+        .filter(line => line.length > 0)
+        .join('\n');
+}
+
+describe('extendDocument', () => {
+    const baseDocument = graphql(`
+        query ProductVariantList($options: ProductVariantListOptions) {
+            productVariants(options: $options) {
+                items {
+                    id
+                    name
+                    sku
+                    price
+                }
+                totalItems
+            }
+        }
+    `);
+
+    it('should add new fields to existing query', () => {
+        const extended = extendDocument(
+            baseDocument,
+            `
+            query ProductVariantList($options: ProductVariantListOptions) {
+                productVariants(options: $options) {
+                    items {
+                        reviewRating
+                        customField
+                    }
+                }
+            }
+            `,
+        );
+
+        const printed = print(extended);
+
+        expect(expectedSDL(printed)).toBe(
+            expectedSDL(`
+                query ProductVariantList($options: ProductVariantListOptions) {
+                    productVariants(options: $options) {
+                        items {
+                            id
+                            name
+                            sku
+                            price
+                            reviewRating
+                            customField
+                        }
+                        totalItems
+                    }
+                }
+            `),
+        );
+    });
+
+    it('should merge nested selection sets', () => {
+        const extended = extendDocument(
+            baseDocument,
+            `
+            query ProductVariantList($options: ProductVariantListOptions) {
+                productVariants(options: $options) {
+                    items {
+                        featuredAsset {
+                            id
+                            name
+                        }
+                    }
+                }
+            }
+            `,
+        );
+
+        const printed = print(extended);
+
+        expect(expectedSDL(printed)).toBe(
+            expectedSDL(`
+                query ProductVariantList($options: ProductVariantListOptions) {
+                    productVariants(options: $options) {
+                        items {
+                            id
+                            name
+                            sku
+                            price
+                            featuredAsset {
+                                id
+                                name
+                            }
+                        }
+                        totalItems
+                    }
+                }
+            `),
+        );
+    });
+
+    it('should handle multiple operations', () => {
+        const multiOpDocument = graphql(`
+            query ProductVariantList($options: ProductVariantListOptions) {
+                productVariants(options: $options) {
+                    items {
+                        id
+                        name
+                    }
+                    totalItems
+                }
+            }
+
+            query ProductVariantDetail($id: ID!) {
+                productVariant(id: $id) {
+                    id
+                    name
+                }
+            }
+        `);
+
+        const extended = extendDocument(
+            multiOpDocument,
+            `
+            query ProductVariantList($options: ProductVariantListOptions) {
+                productVariants(options: $options) {
+                    items {
+                        sku
+                    }
+                }
+            }
+
+            query ProductVariantDetail($id: ID!) {
+                productVariant(id: $id) {
+                    sku
+                    price
+                }
+            }
+            `,
+        );
+
+        const printed = print(extended);
+
+        expect(expectedSDL(printed)).toBe(
+            expectedSDL(`
+                query ProductVariantList($options: ProductVariantListOptions) {
+                    productVariants(options: $options) {
+                        items {
+                            id
+                            name
+                            sku
+                        }
+                        totalItems
+                    }
+                }
+                query ProductVariantDetail($id: ID!) {
+                    productVariant(id: $id) {
+                        id
+                        name
+                        sku
+                        price
+                    }
+                }
+            `),
+        );
+    });
+
+    it('should preserve fragments', () => {
+        const fragmentDocument = graphql(`
+            fragment ProductVariantFields on ProductVariant {
+                id
+                name
+            }
+
+            query ProductVariantList($options: ProductVariantListOptions) {
+                productVariants(options: $options) {
+                    items {
+                        ...ProductVariantFields
+                    }
+                    totalItems
+                }
+            }
+        `);
+
+        const extended = extendDocument(
+            fragmentDocument,
+            `
+            fragment ProductVariantFields on ProductVariant {
+                sku
+            }
+
+            query ProductVariantList($options: ProductVariantListOptions) {
+                productVariants(options: $options) {
+                    items {
+                        price
+                    }
+                }
+            }
+            `,
+        );
+
+        const printed = print(extended);
+
+        expect(expectedSDL(printed)).toBe(
+            expectedSDL(`
+                query ProductVariantList($options: ProductVariantListOptions) {
+                    productVariants(options: $options) {
+                        items {
+                            ...ProductVariantFields
+                            price
+                        }
+                        totalItems
+                    }
+                }
+                fragment ProductVariantFields on ProductVariant {
+                    id
+                    name
+                }
+                fragment ProductVariantFields on ProductVariant {
+                    sku
+                }
+            `),
+        );
+    });
+
+    it('should work with template string interpolation', () => {
+        const fieldName = 'reviewRating';
+        const extended = extendDocument(
+            baseDocument,
+            `
+            query ProductVariantList($options: ProductVariantListOptions) {
+                productVariants(options: $options) {
+                    items {
+                        ${fieldName}
+                    }
+                }
+            }
+            `,
+        );
+
+        const printed = print(extended);
+
+        expect(expectedSDL(printed)).toBe(
+            expectedSDL(`
+                query ProductVariantList($options: ProductVariantListOptions) {
+                    productVariants(options: $options) {
+                        items {
+                            id
+                            name
+                            sku
+                            price
+                            reviewRating
+                        }
+                        totalItems
+                    }
+                }
+            `),
+        );
+    });
+
+    it('should handle the gqlExtend utility function', () => {
+        const extender = gqlExtend`
+            query ProductVariantList($options: ProductVariantListOptions) {
+                productVariants(options: $options) {
+                    items {
+                        reviewRating
+                    }
+                }
+            }
+        `;
+
+        const extended = extender(baseDocument);
+        const printed = print(extended);
+
+        expect(expectedSDL(printed)).toBe(
+            expectedSDL(`
+                query ProductVariantList($options: ProductVariantListOptions) {
+                    productVariants(options: $options) {
+                        items {
+                            id
+                            name
+                            sku
+                            price
+                            reviewRating
+                        }
+                        totalItems
+                    }
+                }
+            `),
+        );
+    });
+
+    it('should not duplicate existing fields', () => {
+        const extended = extendDocument(
+            baseDocument,
+            `
+            query ProductVariantList($options: ProductVariantListOptions) {
+                productVariants(options: $options) {
+                    items {
+                        id
+                        name
+                        reviewRating
+                    }
+                }
+            }
+            `,
+        );
+
+        const printed = print(extended);
+
+        expect(expectedSDL(printed)).toBe(
+            expectedSDL(`
+                query ProductVariantList($options: ProductVariantListOptions) {
+                    productVariants(options: $options) {
+                        items {
+                            id
+                            name
+                            sku
+                            price
+                            reviewRating
+                        }
+                        totalItems
+                    }
+                }
+            `),
+        );
+    });
+
+    it('should merge nested selection sets for existing fields', () => {
+        const baseWithNested = graphql(`
+            query ProductVariantList($options: ProductVariantListOptions) {
+                productVariants(options: $options) {
+                    items {
+                        id
+                        featuredAsset {
+                            id
+                        }
+                    }
+                    totalItems
+                }
+            }
+        `);
+
+        const extended = extendDocument(
+            baseWithNested,
+            `
+            query ProductVariantList($options: ProductVariantListOptions) {
+                productVariants(options: $options) {
+                    items {
+                        featuredAsset {
+                            name
+                            preview
+                        }
+                    }
+                }
+            }
+            `,
+        );
+
+        const printed = print(extended);
+
+        expect(expectedSDL(printed)).toBe(
+            expectedSDL(`
+                query ProductVariantList($options: ProductVariantListOptions) {
+                    productVariants(options: $options) {
+                        items {
+                            id
+                            featuredAsset {
+                                id
+                                name
+                                preview
+                            }
+                        }
+                        totalItems
+                    }
+                }
+            `),
+        );
+    });
+
+    it('should ignore different query names and merge by top-level field', () => {
+        const extended = extendDocument(
+            baseDocument,
+            `
+            query DifferentQueryName($options: ProductVariantListOptions) {
+                productVariants(options: $options) {
+                    items {
+                        reviewRating
+                    }
+                }
+            }
+            `,
+        );
+
+        const printed = print(extended);
+
+        expect(expectedSDL(printed)).toBe(
+            expectedSDL(`
+                query ProductVariantList($options: ProductVariantListOptions) {
+                    productVariants(options: $options) {
+                        items {
+                            id
+                            name
+                            sku
+                            price
+                            reviewRating
+                        }
+                        totalItems
+                    }
+                }
+            `),
+        );
+    });
+
+    it('should ignore different variables and merge by top-level field', () => {
+        const extended = extendDocument(
+            baseDocument,
+            `
+            query ProductVariantList($differentOptions: ProductVariantListOptions) {
+                productVariants(options: $differentOptions) {
+                    items {
+                        reviewRating
+                    }
+                }
+            }
+            `,
+        );
+
+        const printed = print(extended);
+
+        expect(expectedSDL(printed)).toBe(
+            expectedSDL(`
+                query ProductVariantList($options: ProductVariantListOptions) {
+                    productVariants(options: $options) {
+                        items {
+                            id
+                            name
+                            sku
+                            price
+                            reviewRating
+                        }
+                        totalItems
+                    }
+                }
+            `),
+        );
+    });
+
+    it('should throw error when top-level field differs', () => {
+        expect(() => {
+            extendDocument(
+                baseDocument,
+                `
+                query CompletelyDifferentQuery($id: ID!) {
+                    product(id: $id) {
+                        id
+                        name
+                        description
+                    }
+                }
+                `,
+            );
+        }).toThrow("The query extension must extend the 'productVariants' query. Got 'product' instead.");
+    });
+
+    it('should merge anonymous query by top-level field', () => {
+        const extended = extendDocument(
+            baseDocument,
+            `
+            {
+                productVariants {
+                    items {
+                        reviewRating
+                    }
+                }
+            }
+            `,
+        );
+
+        const printed = print(extended);
+
+        expect(expectedSDL(printed)).toBe(
+            expectedSDL(`
+                query ProductVariantList($options: ProductVariantListOptions) {
+                    productVariants(options: $options) {
+                        items {
+                            id
+                            name
+                            sku
+                            price
+                            reviewRating
+                        }
+                        totalItems
+                    }
+                }
+            `),
+        );
+    });
+
+    it('should accept DocumentNode as extension parameter', () => {
+        const extensionDocument = graphql(`
+            query ProductVariantList($options: ProductVariantListOptions) {
+                productVariants(options: $options) {
+                    items {
+                        reviewRating
+                        customField
+                    }
+                }
+            }
+        `);
+
+        const extended = extendDocument(baseDocument, extensionDocument);
+        const printed = print(extended);
+
+        expect(expectedSDL(printed)).toBe(
+            expectedSDL(`
+                query ProductVariantList($options: ProductVariantListOptions) {
+                    productVariants(options: $options) {
+                        items {
+                            id
+                            name
+                            sku
+                            price
+                            reviewRating
+                            customField
+                        }
+                        totalItems
+                    }
+                }
+            `),
+        );
+    });
+});

+ 159 - 0
packages/dashboard/src/lib/framework/document-extension/extend-document.ts

@@ -0,0 +1,159 @@
+import { Variables } from '@/graphql/api.js';
+import type { TypedDocumentNode } from '@graphql-typed-document-node/core';
+import {
+    DefinitionNode,
+    DocumentNode,
+    FieldNode,
+    FragmentDefinitionNode,
+    Kind,
+    OperationDefinitionNode,
+    parse,
+    SelectionNode,
+    SelectionSetNode,
+} from 'graphql';
+
+/**
+ * Type-safe template string function for extending GraphQL documents
+ */
+export function extendDocument<T extends TypedDocumentNode, V extends Variables = Variables>(
+    defaultDocument: T,
+    template: TemplateStringsArray,
+    ...values: any[]
+): T;
+export function extendDocument<T extends TypedDocumentNode, V extends Variables = Variables>(
+    defaultDocument: T,
+    sdl: string | DocumentNode,
+): T;
+export function extendDocument<T extends TypedDocumentNode, V extends Variables = Variables>(
+    defaultDocument: T,
+    template: TemplateStringsArray | string | DocumentNode,
+    ...values: any[]
+): T {
+    // Handle template strings, regular strings, and DocumentNode
+    let extensionDocument: DocumentNode;
+    if (Array.isArray(template)) {
+        // Template string array
+        const sdl = (template as TemplateStringsArray).reduce((result, str, i) => {
+            return result + str + String(values[i] ?? '');
+        }, '');
+        extensionDocument = parse(sdl);
+    } else if (typeof template === 'string') {
+        // Regular string
+        extensionDocument = parse(template);
+    } else {
+        // DocumentNode
+        extensionDocument = template as DocumentNode;
+    }
+
+    // Merge the documents
+    const mergedDocument = mergeDocuments(defaultDocument, extensionDocument);
+
+    return mergedDocument as T;
+}
+
+/**
+ * Merges two GraphQL documents, adding fields from the extension to the base document
+ */
+function mergeDocuments(baseDocument: DocumentNode, extensionDocument: DocumentNode): DocumentNode {
+    const baseClone = JSON.parse(JSON.stringify(baseDocument)) as DocumentNode;
+
+    // Get all operation definitions from both documents
+    const baseOperations = baseClone.definitions.filter(isOperationDefinition);
+    const extensionOperations = extensionDocument.definitions.filter(isOperationDefinition);
+
+    // Get all fragment definitions from both documents
+    const baseFragments = baseClone.definitions.filter(isFragmentDefinition);
+    const extensionFragments = extensionDocument.definitions.filter(isFragmentDefinition);
+
+    // Merge fragments first (extensions can reference them)
+    const mergedFragments = [...baseFragments, ...extensionFragments];
+
+    // For each operation in the extension, find the corresponding base operation and merge
+    for (const extensionOp of extensionOperations) {
+        // Get the top-level field name from the extension operation
+        const extensionField = extensionOp.selectionSet.selections[0] as FieldNode;
+        if (!extensionField) {
+            throw new Error('Extension query must have at least one top-level field');
+        }
+
+        // Find a base operation that has the same top-level field
+        const baseOp = baseOperations.find(op => {
+            const baseField = op.selectionSet.selections[0] as FieldNode;
+            return baseField && baseField.name.value === extensionField.name.value;
+        });
+
+        if (!baseOp) {
+            const validQueryFields = baseOperations
+                .map(op => {
+                    const field = op.selectionSet.selections[0] as FieldNode;
+                    return field ? field.name.value : 'unknown';
+                })
+                .join(', ');
+            throw new Error(
+                `The query extension must extend the '${validQueryFields}' query. ` +
+                    `Got '${extensionField.name.value}' instead.`,
+            );
+        }
+
+        // Merge the selection sets of the matching top-level fields
+        const baseFieldNode = baseOp.selectionSet.selections[0] as FieldNode;
+        if (baseFieldNode.selectionSet && extensionField.selectionSet) {
+            mergeSelectionSets(baseFieldNode.selectionSet, extensionField.selectionSet);
+        }
+    }
+
+    // Update the document with merged definitions
+    (baseClone as any).definitions = [...baseOperations, ...mergedFragments];
+
+    return baseClone;
+}
+
+/**
+ * Merges two selection sets, adding fields from the extension to the base
+ */
+function mergeSelectionSets(
+    baseSelectionSet: SelectionSetNode,
+    extensionSelectionSet: SelectionSetNode,
+): void {
+    const baseFields = baseSelectionSet.selections.filter(isFieldNode);
+    const extensionFields = extensionSelectionSet.selections.filter(isFieldNode);
+
+    for (const extensionField of extensionFields) {
+        const existingField = baseFields.find(field => field.name.value === extensionField.name.value);
+
+        if (existingField) {
+            // Field already exists, merge their selection sets if both have them
+            if (existingField.selectionSet && extensionField.selectionSet) {
+                mergeSelectionSets(existingField.selectionSet, extensionField.selectionSet);
+            } else if (extensionField.selectionSet && !existingField.selectionSet) {
+                // Extension has a selection set but base doesn't, add it
+                (existingField as any).selectionSet = extensionField.selectionSet;
+            }
+        } else {
+            // Field doesn't exist, add it
+            (baseSelectionSet as any).selections.push(extensionField);
+        }
+    }
+}
+
+/**
+ * Type guards
+ */
+function isOperationDefinition(value: DefinitionNode): value is OperationDefinitionNode {
+    return value.kind === Kind.OPERATION_DEFINITION;
+}
+
+function isFragmentDefinition(value: DefinitionNode): value is FragmentDefinitionNode {
+    return value.kind === Kind.FRAGMENT_DEFINITION;
+}
+
+function isFieldNode(value: SelectionNode): value is FieldNode {
+    return value.kind === Kind.FIELD;
+}
+
+/**
+ * Utility function to create a template string tag for better DX
+ */
+export function gqlExtend(strings: TemplateStringsArray, ...values: any[]) {
+    return (defaultDocument: DocumentNode) => extendDocument(defaultDocument, strings, ...values);
+}

+ 14 - 1
packages/dashboard/src/lib/framework/extension-api/define-dashboard-extension.ts

@@ -1,4 +1,5 @@
-import { addBulkAction } from '@/framework/data-table/data-table-extensions.js';
+import { addBulkAction, addListQueryDocument } from '@/framework/data-table/data-table-extensions.js';
+import { parse } from 'graphql';
 
 import { registerDashboardWidget } from '../dashboard-widget/widget-extensions.js';
 import { addCustomFormComponent } from '../form-engine/custom-form-component-extensions.js';
@@ -91,6 +92,18 @@ export function defineDashboardExtension(extension: DashboardExtension) {
                         addBulkAction(dataTable.pageId, dataTable.blockId, action);
                     }
                 }
+                if (dataTable.extendListDocument) {
+                    const document =
+                        typeof dataTable.extendListDocument === 'function'
+                            ? dataTable.extendListDocument()
+                            : dataTable.extendListDocument;
+
+                    addListQueryDocument(
+                        dataTable.pageId,
+                        dataTable.blockId,
+                        typeof document === 'string' ? parse(document) : document,
+                    );
+                }
             }
         }
         const callbacks = globalRegistry.get('extensionSourceChangeCallbacks');

+ 6 - 0
packages/dashboard/src/lib/framework/extension-api/extension-api-types.ts

@@ -1,5 +1,6 @@
 import { PageContextValue } from '@/framework/layout-engine/page-provider.js';
 import { AnyRoute, RouteOptions } from '@tanstack/react-router';
+import { DocumentNode } from 'graphql';
 import { LucideIcon } from 'lucide-react';
 import type React from 'react';
 
@@ -137,6 +138,11 @@ export interface DashboardDataTableDefinition {
      * An array of additional bulk actions that will be available on the data table.
      */
     bulkActions?: BulkAction[];
+    /**
+     * @description
+     * Allows you to extend the list document for the data table.
+     */
+    extendListDocument?: string | DocumentNode | (() => DocumentNode | string);
 }
 
 /**

+ 2 - 0
packages/dashboard/src/lib/framework/registry/registry-types.ts

@@ -1,3 +1,4 @@
+import { DocumentNode } from 'graphql';
 import React from 'react';
 
 import { DashboardAlertDefinition } from '../alert/types.js';
@@ -20,4 +21,5 @@ export interface GlobalRegistryContents {
     dashboardAlertRegistry: Map<string, DashboardAlertDefinition>;
     customFormComponents: Map<string, React.FunctionComponent<CustomFormComponentInputProps>>;
     bulkActionsRegistry: Map<string, BulkAction[]>;
+    listQueryDocumentRegistry: Map<string, DocumentNode[]>;
 }

+ 17 - 0
packages/dev-server/test-plugins/reviews/dashboard/index.tsx

@@ -72,6 +72,23 @@ export default defineDashboardExtension({
                     ),
                 },
             ],
+            extendListDocument: `
+                query {
+                    products {
+                        items {
+                            customFields {
+                                featuredReview {
+                                    id
+                                    productVariant {
+                                        id
+                                        name
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            `,
         },
     ],
 });