Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Eager fetching of relation custom fields in Admin UI causing performance issues #3097

Closed
michaelbromley opened this issue Sep 30, 2024 · 1 comment
Labels
Milestone

Comments

@michaelbromley
Copy link
Member

Describe the bug
Currently, the Admin UI will automatically add custom field selections to any fragment of a type which has custom fields defined.

For example, let's say we have defined a featuredCollection custom field on Product, which is a relation type to the Collection entity.

When executing the product list or detail query, that featuredCollection custom field will be added to the field selection (all of its primitive fields will be expanded).

This is convenient but in the case that there are lots of relation custom fields defined, it can cause severe performance issues where the resulting query is so complex that it can take a very long time or even bring down the server due to memory use at the DB level in extreme cases.

Expected behavior
One should be able to define any number of custom field relations without causing critical performance issues in the Admin UI.

  • List queries should only include custom field relations if they are actually being displayed in the data table.
  • For detail pages, another solution is required: possibly lazy-loading in a separate query.

Environment (please complete the following information):

  • @vendure/core version: 3.0.3
  • Nodejs version: any
  • Database (mysql/postgres etc): any
@michaelbromley michaelbromley moved this to ♻️ In progress in Vendure OS Roadmap Sep 30, 2024
@michaelbromley michaelbromley added this to the v3.0.4 milestone Sep 30, 2024
@michaelbromley
Copy link
Member Author

This is an extremely hard problem to solve. I'm noting down avenues of research just as a record of what has been attempted:

ApolloLink

I tried setting up an ApolloLink that will:

  1. add custom fields to the DocumentNode
  2. refetch on changes to the customField selection
import { NextLink, Observable as ZenObservable, Operation } from '@apollo/client/core';
import { createOperation } from '@apollo/client/link/utils/index';
import { from, Observable, of, switchMap } from 'rxjs';
import { ApolloLink } from '@apollo/client/core';
import { FetchResult } from '@apollo/client/link/core/types';
import { addCustomFields, ServerConfigService } from '@vendure/admin-ui/core';
import { omit } from '@vendure/common/lib/omit';
import { startWith } from 'rxjs/operators';

/**
 * Adds custom fields to the documentNode.
 */
export class AddCustomFieldsLink extends ApolloLink {
    constructor(private serverConfigService: ServerConfigService) {
        super((operation, forward) => {
            // todo: use getContext() to get the array of custom fields to include
            // using the addCustomFields() function
            const customFieldsToAdd =
                (operation.getContext().customFields as Observable<string[]>) ?? of(undefined);

            const wrappedObservable = new ZenObservable<FetchResult>(observer => {
                const sub = customFieldsToAdd
                    .pipe(
                        startWith(undefined),
                        switchMap(fields => {
                            console.log(`customFields: ${fields}`);
                            // operation.query = addCustomFields(
                            //     operation.query,
                            //     this.serverConfigService.customFieldsMap,
                            //     fields,
                            // );
                            const newOperation = createOperation(operation.getContext(), {
                                ...operation,
                                query: addCustomFields(
                                    operation.query,
                                    this.serverConfigService.customFieldsMap,
                                    fields,
                                ),
                            });
                            return zenToRx(forward(newOperation));
                        }),
                    )
                    .subscribe((result: any) => {
                        observer.next(result);
                    });

                return () => sub.unsubscribe();
            });
            return wrappedObservable ?? null;
        });
    }
}

const zenToRx = <T>(zenObservable: ZenObservable<T>): Observable<T> =>
    new Observable(observer => zenObservable.subscribe(observer));

Problem:

  • Dynamically modifying the DocumentNode at this point does not work with Apollo's internal cache. Adding new fields to the object will result in the error:
Missing field 'customFields' while writing result

It seems that the Apollo cache shape is set earlier in the request process, and the returning object must conform to the known fields that are set in the cache at that time.

michaelbromley added a commit that referenced this issue Oct 4, 2024
This commit introduces some new low-level APIs to the data layer
of the Admin UI. It allows us to control which custom fields
get dynamically added to fragments when making queries & mutations.

It also exposes a new method on the QueryResult class which allows
us to update & refetch the underlying DocumentNode whenever
the selected custom fields changes.

Relates to #3097
@michaelbromley michaelbromley moved this from 💯 Ready to 🚀 Shipped in Vendure OS Roadmap Oct 4, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
Archived in project
Development

No branches or pull requests

1 participant