GraphQL динамическое построение запросов

У меня есть сервер GraphQL, который может обслуживать данные временных рядов для указанного источника (например, данные датчика). Пример запроса для извлечения данных для датчика может быть:

query fetchData {
    timeseriesData(sourceId: "source1") {
      data {
        time
        value
      }
    }
}

В моем интерфейсе я хочу позволить пользователю выбрать 1 или более источников и показать диаграмму с линией для каждого из них. Кажется, что это было бы возможно с помощью запроса, подобного этому:

query fetchData {
    series1: timeseriesData(sourceId: "source1") {
      data {
        time
        value
      }
    }
    series2: timeseriesData(sourceId: "source2") {
      data {
        time
        value
      }
    }
}

Большинство учебников по GraphQL, похоже, сосредоточены на статических запросах (например, когда единственное, что меняется, - это переменные, а не фактическая форма запроса), но в моем случае мне нужен сам запрос, чтобы он был динамическим (один запрос timeseriesData для каждого из моих выбранных идентификаторов).

У меня есть следующие ограничения:

  1. Изменение схемы сервера не вариант (поэтому я не могу передать массив идентификаторов, например, для резольвера)
  2. Мой запрос указан с использованием строки шаблона, например, gql`...`
  3. Я не хочу вручную формировать запрос в виде строки, потому что это похоже на рецепт катастрофы и будет означать, что я потеряю все преимущества инструментов (например, автозаполнение, выделение синтаксиса, линтинг)

Стек, который я использую:

  • Аполлон-клиент (в частности, аполлон-угловой)
  • угловатый
  • Машинопись
  • graphql-tag (для определения запросов)

В идеале я хочу объединить два запроса в один, чтобы я мог определить их в соответствии с первым примером, а затем соединить их вместе на уровне абстракции, чтобы получить один запрос, как во втором примере, чтобы быть отправлено по проводу.

Однако я не уверен, как этого добиться, потому что graphql-tag анализирует запрос в AST, и я пытаюсь понять, выполнимо ли манипулировать запросом таким образом.

Какие существуют методы для генерации такого динамического запроса, когда форма запроса не известна заранее?

6 ответов

Вы можете использовать фрагмент, чтобы определить общие поля и границы переменных @include(if: Boolean) а также @skip(if: Boolean) директивы для этого фрагмента, чтобы получить динамические поля, которые известны во время выполнения.

Я думаю, у вас нет другого выбора, кроме использования функций String, когда пользователь выбирает датчики динамически, и даже вы не знаете датчики во время разработки (не во время выполнения).

const mainQuery = gql
  `query fetchData($sourceId: String!) {
    timeseriesData(sourceId: $sourceId) {
      data {
        time
        value
      }
    }
  }`;

const mainQueryStr = mainQuery.loc.source.body;

В mainQueryStr - строковое значение вашего запроса (чтобы справиться с динамикой вашей проблемы). Затем зацикливайтесь на датчиках и замените $sourceId с идентификатором каждого датчика

// You have to remove the query wrapper first
// Then replace sensor id
const sensorsQueries = sensors.map(sid => mainQueryStr
  .split(`\n`)
  .slice(1, 7)
  .replace(`$sourceId`, sid)
)

Затем вам следует присоединиться к sensorQueries и сделать новый запрос GraphQL.

const finalQuery = gql`
  query fetchData {
    ${sensorsQueries.join(`\n`)}`
  };

В этом случае вы можете использовать такие преимущества инструментов, как автозаполнение, подсветка синтаксиса и... для mainQuery запрос, а не finalQuery (Из-за того, что вы создаете это динамически)

Я думаю, что вы могли бы использовать фрагменты для этого! Но вы все равно должны написать 2 "queries" в этом случае fragments,

Сначала давайте создадим fragment для каждого timeSeriesПожалуйста, проверьте ваш тип запроса timeSeries, я буду ссылаться на него как timeseriesDataQuery

const series1Q = gql`
  fragment series1 on timeseriesDataQuery {
    series1: timeseriesData(sourceId: "source1") {
      data {
        time
        value
      }
    }
  }
}

const series2Q = gql`
  fragment series2 on timeseriesDataQuery {
    series2: timeseriesData(sourceId: "source2") {
      data {
        time
        value
      }
    }
  }
}

А затем просто сшить их в запросе:

export const mainQuery = gql`
    query fetchData {
      ...series1 
      ...series2
    }
    ${series1Q}
    ${series2Q}
`    

Имейте в виду, что запрос - это просто строка. Вы можете использовать простые шаблонные литералы для выполнения своего динамического запроса.

      const generateQuery = () => {
  let query = ""

  for (let i = 1; i < 3; i++) {
    const series = `series${i}`
    const source = `source${i}`

    query += `
      ${series}: timeseriesData(sourceId: "${source}") {
        prices
        queried
      }
    `
  }

  return query
}

const fetchDataQuery = gql`
  query fetchData {
    ${generateQuery()}
  }
`

Для тех, кто чувствует, что строковые операции не являются современным способом обработки кодирования, есть библиотека для программного построения запросов для GraphQL: gql-query-builder.

Решение состоит в том, чтобы запустить несколько сообщений с использованием GRAPHQL, разделив первый ответ функции и передав значение второй функции с помощью динамически созданного запроса GRAPHQL.

mainQueries.ts

      export const generalLandingPageBySlugQuery: string = `
query ($slug: String!, $isPreview: Boolean = false, $locale: String!) {
  templateGeneralLandingPageCollection(where: {slug: $slug}, locale: $locale, preview: $isPreview, limit: 1) {
    items {
      title
      slug
      metadata {
        ...metadataFields
      }
      components: componentsCollection(limit: 10) {
        items {
          __typename
          ... on TextCardsComponent {
            ...textCardsFields
          }
          ... on TwoColumnImageTextComponent {
            ...twoColumnImageTextFields
          }
        }
      }
    }
  }
} ${metadataFields}
${components.twoColumnImageText}
${components.accordion}
`;

Фрагменты.ts

      export const components = {
textCards: `fragment textCardsFields on TextCardsComponent {
    rtHeading: heading {
      json
    }
    backgroundColor
    links: linksCollection {
      items {
        title
        url
        openInNewTab
      }
    }
  }`,
  twoColumnImageText: `
  fragment twoColumnImageTextFields on TwoColumnImageTextComponent {
    rtTitle:title {
      json
    }
    variant
    backgroundColor
    rtHeading: heading {
      json
    }
    rtBlurb: blurb {
      json
    }
    cta {
      title
      url
      openInNewTab
    }
    eyebrow
    cardTitle
    cardDescription
    cardLink
    image {
      ...assetFields
    }
    videoType
    videoId
  }`,

Angular Service.ts Функция первая

         generalLandingPageBySlug(slug: string) {
        const queryVariables = JSON.stringify({
          slug,
          isPreview: this.isPreview,
          locale: this.coreService.locale.code,
        });
    
        return this.http
          .post<ContentfulApiResponse>( // REQUEST ONE
            environment.local ? LOCAL_GRAPHQL : `${GRAPHQL}/general-lp-${slug}`,
            {
              query: Utils.compressGraphQl(generalLandingPageBySlugQuery),
              variables: queryVariables,
            }
          )
          .pipe(
            map((response: ContentfulApiResponse) => {
              this.typename =
                response.data.templateGeneralLandingPageCollection?.items[0].components.items;
    
              //Components Lists
              const currentComponents = [
                ...new Map(
                  this.typename.map((obj: any) => [JSON.stringify(obj), obj])
                ).values(),
              ];
    
              this.typename = currentComponents;
              const choosenComponents = this.typename.map(function (typeName: {
                __typename: any;
              }) {
                return typeName.__typename;
              });
             
                 //Dynamic Query
                  const queryData = 
                'query ($slug: String!, $isPreview: Boolean = false, $locale: String!) {' +
                'templateGeneralLandingPageCollection(where: {slug: $slug}, locale: $locale, preview: $isPreview, limit: 1) {' +
                'items {' +
                'title ' +
                'slug ' +
                'metadata {' +
                '...metadataFields' +
                '}' +
                'components: componentsCollection(limit: 15) {' +
                'items {' +
                '__typename ' +
                (choosenComponents.includes('TextCardsComponent')
                  ? '... on TextCardsComponent {...textCardsFields}'
                  : '') +
                (choosenComponents.includes('TwoColumnImageTextComponent')
                  ? '... on TwoColumnImageTextComponent {...twoColumnImageTextFields}'
                  : '') +
                '}' +
                '}' +
                '}' +
                '}' +
                '}' +
                'fragment metadataFields on Metadata{title metaTitle keyword description facebookType image{...assetFields}canonical noIndex} ' +
                (choosenComponents.includes('TextCardsComponent')
                  ? 'fragment textCardsFields on TextCardsComponent{rtHeading:heading{json}backgroundColor links:linksCollection{items{title url openInNewTab}}}'
                  : '') +
                (choosenComponents.includes('TwoColumnImageTextComponent')
                  ? 'fragment twoColumnImageTextFields on TwoColumnImageTextComponent{rtTitle:title{json}variant backgroundColor rtHeading:heading{json}rtBlurb:blurb{json}cta{title url openInNewTab}eyebrow cardTitle cardDescription cardLink image{...assetFields}videoType videoId}'
                  : '') +
                 ';
              return queryData;
            }),
            mergeMap((payload) => {
              return this.generalFinalData(payload, slug);
            })
          );
      }

Функция вторая

        generalFinalData(payload: string, slug: string) {
    const queryVariables = JSON.stringify({
      slug,
      isPreview: this.isPreview,
      locale: this.coreService.locale.code,
    });
    return this.http
      .post<ContentfulApiResponse>( // REQUEST TWO
        environment.local ? LOCAL_GRAPHQL : `${GRAPHQL}/general-lp-${slug}`,
        {
          query: Utils.compressGraphQl(payload),
          variables: queryVariables,
        }
      )
      .pipe(
        map((response: ContentfulApiResponse) => {
          let contentfulResponse: ContentfulResponse = {
            __typename: '',
            pageData: {},
            components: [],
          };
          return this.hasError(
            response,
            response.data.templateGeneralLandingPageCollection
          )
            ? contentfulResponse
            : this.buildData(
                this.unwrapResponse(
                  response.data.templateGeneralLandingPageCollection
                ),
                contentfulResponse
              );
        })
      );
  }
Другие вопросы по тегам