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 для каждого из моих выбранных идентификаторов).
У меня есть следующие ограничения:
- Изменение схемы сервера не вариант (поэтому я не могу передать массив идентификаторов, например, для резольвера)
- Мой запрос указан с использованием строки шаблона, например, gql`...`
- Я не хочу вручную формировать запрос в виде строки, потому что это похоже на рецепт катастрофы и будет означать, что я потеряю все преимущества инструментов (например, автозаполнение, выделение синтаксиса, линтинг)
Стек, который я использую:
- Аполлон-клиент (в частности, аполлон-угловой)
- угловатый
- Машинопись
- 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
);
})
);
}