Jak připojit GraphQL API do Power BI jako datový zdroj
- Vojtěch Šíma
- Aug 4
- 19 min read
tl;dr GraphQL API je jen další způsob, jak si aplikace mezi sebou povídají a tahají si tasty data. Na rozdíl od RESTu, kde voláš různé endpointy, v GraphQL pošleš jeden dotaz jako string, ve kterém přesně řekneš, co chceš, a server to sám nějak poskládá. V tomhle představení GraphQL ti ukážu klíčové pojmy jako proměnné, aliasy, fragmenty, direktivy a další. A hlavně, jak to celé napojit do Power BI pomocí našeho oblíbeného Mka.
Jako příklady, využiji tyto volné API: https://countries.trevorblades.com/; https://rickandmortyapi.com/graphql
Table of Contents:
Proč bys měl vědět o GraphQL?
Experimentování s automatickým vytvářením typovaných tabulek z GraphQL
Proč bys měl vědět o GraphQL?
GraphQL je poměrně nový druh webového API. "Poměrně nový" znamená, že uběhlo 10 let od prvního vydání, nicméně stabilní verze je mezi námi teprve pár posledních let. Hlavní důvod, proč je GraphQL cool, je ten, že oproti klasickému RESTu nemusíš posílat dvě stě requestů na třista různých endpointů. V GraphQL pošleš jeden požadavek, kde přesně nadefinuješ, co chceš dostat, a dostaneš přesně to. To, jaké endpointy se volají, neřešíš ty, ale server to zmákne za tebe.
"Ask for what you need, get exactly that" -GraphQL autoři
Jinými slovy, pokud jsi někdy psal SQLko, tzv. Structured Query Language, s GraphQL můžeš psát "GQLko", protože QL v GraphQL znamená také Query Language.
„GQLko“ na veřejnosti pravděpodobně ještě nikdo nikdy nepoužil, ale pokud chceš, klidně buď první.
Tady máš ukázku
Pro ty, co moc nečtou (jako třeba já) a jenom skenují články pro klíčové pojmy, pošlu ti hned na senzor obrázek s příkladem, abys uspokojil svoje dopaminové receptory.

jinak nejsem uplně fanoušek Star Wars, příklad je z oficiálních webovek
V příkladě můžeš vidět, jak můžeš poskládat query tím, že vypisuješ potřebná pole, případně pole polí, pokud jmenuješ objekt. Možná ti to trošku připomíná JSON, což je fér, protože struktura může působit podobně. Nicméně GraphQL je svůj vlastní jazyk, takže i když najdeš nějaké podobnosti, není to totéž. Ale pokud ti pomůže o tom přemýšlet jako o JSONu, tak klidně můžeš.
Rychlovka v Mku
Pokud jsi přišel jenom zkopírovat rychlý kód, máš ho mít. Nicméně doporučuju sečkat i na zbytek článku, kde vysvětlím důležité koncepty GraphQL, aby sis udělal celý obrázek.
V jazyce M použiješ funkci Web.Contents() k odeslání GraphQL dotazu. Samotný dotaz se dává do parametru Content. Většina nastavení je podobná jako u REST API, jen mysli na to, že posíláš POST request. Jak už jsem říkal, autentizace funguje v podstatě stejně (pokud chceš jít víc do hloubky, mrkni na můj jiný článek).
Tady máš příklad, jak pomocí toho ukázkového API, co jsem zmiňoval, získáš seznam evropských zemí bez nutnosti autentizace.
let
query = "
{
continent(code: ""EU"") {
name
countries {
code
name
}
}
}",
requestBody = Json.FromValue([query = query]),
response = Web.Contents(
"https://countries.trevorblades.com/",
[
Headers = [#"Content-Type" = "application/json"],
Content = requestBody
]
),
parsed = Json.Document(response)
in
parsed Pokud chceš tabulku jako výsledek, přidej následující:
let
/* the stuff before parsed */
parsed = Json.Document(response),
// transform data into Table
getTableFancy =
#table(
type table [
continentName = text,
countryName = text,
countryCode = text
],
let
continent = parsed[data]?[continent]?[name]?,
countries = parsed[data]?[continent]?[countries]?
in
List.Transform(
countries,
each {
continent,
[name]?,
[code]?
}
)
)
in
getTableFancyA to je pro začátek víceméně všechno. Teď už půjdeme do hloubky každého GraphQL prvku a ukážeme si, jak ho napsat v Mku.
Btw, názvy konceptů v nadpisech překládat nebudu, protože pak se podle nich blbě hledá dokumentace. Potom uvnitř odstavce se může stát, že je přeložím, nebo se je budu snažit skloňovat.
GraphQL koncepty & Implementace v Mku
Základní struktura dotazu
Countries API příklad
GraphQL dotaz používá složené závorky {} k definování polí, která chceš načíst. Minimálně musíš uvést aspoň jedno root pole. Každé pole pak může mít svoje podpole, a pokud chceš vrátit víc polí nebo podpolí, prostě je pod sebe vyskládáš. Žádné čárky, žádné středníky, žádná podivná interpunkce. Dokonce to ani nemusíš hezky formátovat. GraphQL ignoruje "white-space", takže technicky můžeš napsat celý dotaz na jeden ošklivý řádek, pokud to tak tvoje srdíčko cítí.
{ continent(code: ""EU"") { name countries{ code name } } }Funkční, ale není to vončo.
Na co si dát bacha: pokud zahrneš pole, které čeká podpole, ale žádná nespecifikuješ, dotaz ti vyhodí chybu. Některé GraphQL explorery (často zabudované přímo v dokumentaci API) ti sice můžou ukázat, že je všechno v pohodě, automaticky ti tam přihodí, co chybí. Například zvolíš pole inputFields a ono ti tiše doplní další věci. Ale to je jen editor, co ti chce pomoct. Skutečný GraphQL server ti bez explicitně uvedených podpolí stejně vyhodí chybu.
Pokud chceš, aby tvoje dotazy vypadaly trochu víc standardně, můžeš (nebo spíš bys měl) začínat dotaz klíčovým slovem query. Je nepovinné, pokud se nepouštíš do složitějších věcí. GraphQL podporuje tři typy operací:
query pro získávání dat (tomu se tu budeme věnovat)
mutation pro zápis nebo změny dat
subscription pro real-time aktualizace
Mutacím a subscriptionům se v tomhle článku věnovat nebudeme, ale je fajn o nich vědět.
Ještě malý tip: můžeš svůj dotaz pojmenovat. Není to povinné, ale dost se to hodí pro debug nebo opakované použití. Některé API ti dokonce můžou nařídit pojmenovat si query.
query GetEuropeanCountries {
continent(code: "EU") {
name
countries {
code
name
}
}
}Field Arguments
Obecný příklad
Možná sis v předchozím příkladu všiml, že jsem nepoužil jen složené závorky, ale taky závorky kulaté (). Stejně jako v Mku se do nich předávají argumenty, a přesně to samé platí i tady.
Když nějaké pole argumenty vyžaduje nebo je podporuje, napíšeš název pole s kulatými závorkami a uvnitř uvedeš jeden nebo víc párů klíč: hodnota.
Argumenty se píšou ve formátu klíč: hodnota (key:value), přičemž klíč není v uvozovkách a hodnota ano, ale jen když jde o text. Ve výchozím stavu GraphQL podporuje základní datové typy jako Int, Float, String, Boolean, a ID. Spousta API si ale přidává i vlastní skaláry nebo enumy/enums podle toho, co zrovna potřebují.
Enums jsou speciální typy s pevně daným seznamem povolených hodnot, jako třeba ACTIVE nebo INACTIVE. Nejde o stringy, takže je nedávej do uvozovek. Prostě je napiš tak, jak jsou definované.
Argumenty se hodí třeba pro filtrování, výběr konkrétních objektů (například podle ID), nebo i pro úpravu výsledku. Některá pole například berou jako argument formátovací volby nebo datumové filtry.
Hodnoty argumentů můžou být skaláry, enumy nebo vstupní objekty. Pokud je argument povinný, schéma ho označí vykřičníkem !. Při psaní dotazu platí: stringy do uvozovek, čísla a booleany bez, enumy taky bez uvozovek a objekty se píšou ve složených závorkách jako {} s páry klíč: hodnota.
Tady je příklad, jak to může vypadat (náhodný, netýká se Countries API):
accounts(filter: { status: ACTIVE, minBalance: 1000 })Introspection Queries
Rick & Morty API příklad
Abychom věděli, podle čeho můžeme filtrovat nebo jaké enumy máme k dispozici, musíme nejdřív pochopit schéma, se kterým pracujeme. Jasně, můžeš to proklikat v dokumentaci, ale jestli radši objevuješ věci jako Kluci v akci, GraphQL ti dovolí dotazovat se přímo na metadata.
Tak si řekněme, že si trochu "zkoumám postavy" a chci zjistit, co všechno v tom API vlastně je. Začnu dotazem přes __schema, který mi řekne, jaké entry pointy mám k dispozici. Vypíše všechny dostupné query, takže vím, na co se můžu server zeptat. Ber to jako takovou top-level mapu toho, co je vůbec možné.
Pak použiju __type(name: "Characters"), což mi přiblíží konkrétní typ, který vrací dotaz characters. Ukáže mi, jaká pole tenhle typ nabízí, třeba results a info, a co přesně v těch polích je. Potom se podívám i na samotný typ Character, abych zjistil, co všechno obsahuje jeden objekt postavy, například id, name, status, a species.
Jakmile vím, co je k dispozici a jak data vypadají, spustím samotný dotaz characters, abych získal reálná data. A tady pozor. Dotaz se jmenuje characters s malým „c“, typ, který vrací, je Characters s velkým „C“ a objekty uvnitř jsou typu Character.
Takže když se ptáš na typ, použiješ Characters s velkým „C“. Ale když to chceš použít přímo ve svém dotazu, píšeš characters s malým. Je dost snadné si to splést a pak dostaneš zbytečné chyby, obzvlášť když přepínáš mezi introspekcí a reálnými dotazy.
Krok 1: Zjisti, jaké operace API nabízí
query GetRootOperations {
__schema {
queryType {
fields {
name
description
type {
name
kind
}
}
}
}
}
Krok 2: Prozkoumej strukturu typu, který vrací Characters
query GetCharactersReturnType {
__type(name: "Characters") {
name
kind
fields {
name
type {
kind
name
ofType {
name
kind
}
}
}
}
}
Krok 3: Mrkni, co všechno obsahuje jeden objekt typu Character
query GetCharacterFields {
__type(name: "Character") {
name
fields {
name
type {
kind
name
}
}
}
}
Krok 4: Načti reálná data o postavách
query GetAllCharacters {
characters {
results {
id
name
species
status
}
}
}
Inline Fragments
Obecný příklad
Když dotazuješ pole, které vrací interface nebo union typ, pracuješ se situací, kdy se mohou vrátit různé typy objektů a každý typ může potřebovat jiná podpole. Pomocí inline fragmentů si můžeš přesně určit, která pole chceš u kterého typu načíst.
Představ si třeba, že máš dotaz search , který kontroluje mediální výskyt osoby a vrací typy jako Blog, Video nebo Photo. Každý z těchto typů má jiná pole. Některá můžou být společná, třeba title, pokud implementují stejný interface, ale zbytek je specifický jako třeba wordCount pro blogy, duration pro videa nebo size pro fotky.
Může to vypadat třeba takhle:
{
search(text: "graphql") {
results {
title
... on Blog {
wordCount
}
... on Video {
duration
}
... on Photo {
size
}
}
}
}Fragments
Rick & Morty API příklad
Když máš dotaz, který vrací objekty se stejnými poli a nechceš se pořád opakovat, fragmenty ti kryjou záda. Můžeš si nadefinovat fragment se seznamem polí, která chceš vždycky načíst, a pak místo ručního vypisování tenhle fragment jen zavoláš.
Chová se to úplně stejně, jako bys ta pole napsal ručně, ale vypadá to mnohem čistěji.
Tohle se hodí hlavně tehdy, když pracuješ s opakujícími se strukturami, sdílenými typy objektů, nebo stavíš větší dotaz, kde se stejná pole opakují na víc místech.
Fragmenty se definují mimo hlavní query pomocí klíčového slova fragment, následuje název fragmentu a typ, ke kterému se vztahuje. Můžeš je napsat nad query nebo pod ni.
Potom ho zavoláš přes tři tečky.
query WithFragment {
characters(page: 1) {
results {
...CharacterDetails
}
}
episodes(page: 1) {
results {
name
episode
characters {
...CharacterDetails
}
}
}
}
fragment CharacterDetails on Character {
id
name
status
species
}Variables
Rick & Morty API příklad
Jedním ze základních konceptů GraphQL je použití proměnných. Slouží k tomu, abys nemusel v dotazu natvrdo vypisovat hodnoty a mohl ho udělat dynamický a znovupoužitelný.
V Mku máš často nějaký seznam hodnot, které chceš předat do dotazu. Technicky bys mohl query poskládat jako string a hodnoty do něj rovnou nalepit, ale čistší přístup je použít správnou GraphQL proměnnou. Tedy napsat obecný dotaz, který přijímá vstup přes proměnnou, a hodnoty pak dodat zvlášť ve strukturované podobě.
Proměnné se definují zvlášť. Vytvoříš si slovník (většinou jako JSON) ve formátu nazevPromenne: hodnota.
V samotném dotazu pak místo statické hodnoty použiješ znak dolaru a název proměnné, například $characterId. Pokud používáš proměnné, musíš je nahoře v dotazu taky deklarovat, třeba takhle (název a očekávaný typ):
query ($characterId: ID!) {
character(id: $characterId) { ... }
}Dotazu taky můžeš dát jméno, třeba query query getCharacter(učení proměnných...). Není to povinné, ale hodí se to při ladění nebo když spouštíš víc operací najednou.
Proměnné můžeš použít ve filtrech, argumentech, stránkování nebo klidně i s fragmenty. Prostě všude tam, kde bys jinak něco napsal natvrdo, můžeš použít proměnnou.
Příklad
V tomhle příkladu budeme volat postavy a jejich epizody. Řekněme, že už máš jiný dotaz, který ti vrací seznam ID postav, a teď chceš zjistit, v jakých epizodách se každá z nich objevila.
Začneme tím, že nasimulujeme situaci, kdy API přijímá jen jedno ID na požadavek. To znamená, že projdeš seznam a pro každou postavu zavoláš dotaz zvlášť pomocí proměnné.
Pak ti ukážu, jak to udělat i jedním dotazem. To už funguje s Rick & Morty API, protože to umí přijmout seznam ID přes pole charactersByIds.
query GetCharacter($charId: ID!) {
character(id: $charId) {
id
name
episode {
name
episode
}
}
S proměnnými definovanými takto:
{
"charId": 1
}V Mku proměnné zapisujeme jako samostatné pole uvnitř requestRecord, a v něm je definujeme klasickým způsobem klíč: hodnota. Pokud máš víc ID, ale API podporuje jen jedno ID na požadavek, budeš muset vyřešit procházení seznamu mimo samotný GraphQL. To znamená dotaz si postavit jen jednou a pak v Mku projet celý seznam ID.
Takhle nějak to může vypadat:
let
graphQLQuery = "
query GetCharacter($charId: ID!) {
character(id: $charId) {
id
name
episode {
name
episode
}
}
}",
listOfIds = {1,7,10},
requestFunction = (query as text, id as number)=>
let
requestRecord =
[
query = query,
variables = [charId = id]
],
requestBody = Json.FromValue(requestRecord),
response =
Json.Document(
Web.Contents(
"https://rickandmortyapi.com",
[
RelativePath= "graphql",
Headers = [#"Content-Type" = "application/json"],
Content = requestBody
]
)
)
in
response,
callCharacters =
List.Transform(
listOfIds,
(i)=> requestFunction(graphQLQuery, i)
)
in
callCharactersPokud tvoje API podporuje předání seznamu hodnot, nemusíš vůbec iterovat. Do proměnné jednoduše pošleš seznam.
Místo:
query GetCharacter($charId: ID!) {
character(id: $charId) { # the rest of codes proměnnou:
{ "charId": 1 }By sis napsal:
query GetCharacters($ids: [ID!]!) {
charactersByIds(ids: $ids) { # the rest of codes proměnnou:
{ "ids": [1, 7, 10] }V Mku to pak celé vypadá nějak takto:
let
graphQLQuery = "
query GetCharacters($ids: [ID!]!) {
charactersByIds(ids: $ids) {
id
name
episode {
name
episode
}
}
}",
listOfIds = {1,7,10},
requestFunction = (query as text, id as list)=>
let
requestRecord =
[
query = query,
variables = [ids = id]
],
requestBody = Json.FromValue(requestRecord),
response =
Json.Document(
Web.Contents(
"https://rickandmortyapi.com",
[
RelativePath= "graphql",
Headers = [#"Content-Type" = "application/json"],
Content = requestBody
]
)
)
in
response,
callCharacters = requestFunction(graphQLQuery, listOfIds)
in
callCharactersAliases
Rick & Morty API příklad
Pokud jsi někdy dělal v SQLku, víš, že když chceš vrátit stejný sloupec víckrát, hodí se aliasy. Někdy jsou dokonce nutné. GraphQL to má stejně. Když potřebuješ volat stejné pole víc než jednou, ale s jinými argumenty, alias je tvůj kámoš.
Místo toho, abys jen napsal název pole, přidáš před něj vlastní alias, dvojtečku a pak klasicky název pole (plus případné argumenty, které chceš předat).
Řekněme, že chceme načíst dva typy postav: živé a mrtvé (spoiler alert). Voláme stejné pole characters dvakrát, ale s různými filtry. A protože se opakuje stejná struktura, můžeme rovnou použít fragmenty, ať to vypadá stylověji a líp se s tím pracuje.
query {
aliveChars: characters(filter: { status: "Alive" }) {
results {
...CharacterBasics
}
}
deadChars: characters(filter: { status: "Dead" }) {
results {
...CharacterBasics
}
}
}
fragment CharacterBasics on Character {
id
name
status
}
Filtering
Rick & Morty a Countries API příklad
Když chceš zúžit výsledky, použiješ filtry. Možná sis všiml, že jsem je už dřív nenápadně propašoval do několika dotazů, ale ještě jsem pořádně nevysvětlil, co to vlastně je a jak fungují.
Filtry, na rozdíl od přímých argumentů u polí (jako třeba continent(code: "EU")), ti umožní nastavit víc podmínek najednou. Je to jednoduchý princip, ale super užitečný, když nechceš z API tahat zbytečně moc dat.
Jak zjistit, co můžeš filtrovat
Ne každé pole podporuje filtrování. Aby ses dozvěděl, které ano, můžeš použít introspekční dotazy a mrknout na root-level pole a jejich argumenty.
Tady je jednoduchý dotaz, kterým můžeš začít:
{
__type(name: "Query") {
fields {
name
args {
name
type {
name
kind
}
}
}
}
}
Hledáš pole, která přijímají argument filter. Když na nějaké narazíš, vypadá to třeba takhle:

Podívej se blíž na vstupní typ a zjisti, jaké možnosti filtrování nabízí:
{
__type(name: "FilterCharacter") {
inputFields {
name
type {
name
kind
}
}
}
}Dostaneš seznam všech polí, která můžeš ve filtru použít, včetně jejich datových typů.
Jakmile znáš strukturu, můžeš spustit dotaz s filtrem třeba takhle:
query filteredCharacters {
characters(filter: { status: "alive", species: "human" }) {
results {
species
name
}
}
}A co operátory jako eq či in?
Ve výchozím stavu jsou GraphQL filtry docela jednoduché. Používají jen dvojtečky pro přiřazení hodnot. Pokud API podporuje pokročilejší operátory jako eq, in, contains, nebo startsWith, musí je vývojář definovat přímo ve schématu.
Třeba u Countries API najdeš typ StringQueryOperatorInput, který přesně tohle umožňuje.
Můžeš ho najít introspekcí stejně jako dřív a díky tomu psát dotazy třeba takhle:
{
countries(filter: { continent: { eq: "EU" }, code: { in: ["FR", "DE", "PL"] } }) {
name
code
continent {
name
}
}
}
Directives
Rick & Morty API přklad
Directives ti pomocí dvou klíčových slov umožňují určit, jestli chceš některé části dotazu nebo pole zahrnout, nebo přeskočit. Při tvorbě reportů se to ale moc nehodí, protože většinou nechceš dynamicky měnit strukturu výstupu. To bys report snadno rozbil.
V GraphQL jsou dvě vestavěné direktivy:
@include
@skip
Obě používají klíčové slovo if:, za kterým následuje výraz, co vrací true nebo false.
Napadlo mě ale pár případů, kdy se to může hodit, hlavně při debugování nebo průzkumu API.
Zůstaňme u průzkumu. Můžeš si postavit dotaz, který vrací všechna pole, která pak použiješ ve finálním reportu, a podle přepínače true/false si volitelně zobrazíš i strukturu objektu. To se hodí, když chceš prozkoumat, co všechno je dostupné, aniž bys pořád listoval dokumentací. Během průzkumu to necháš zapnuté a jakmile máš jasno, vypneš.
query ExploreCharacter($id: ID!, $withSchema: Boolean!) {
character(id: $id) {
id
name
status
}
__type(name: "Character") @include(if: $withSchema) {
name
fields {
name
type {
name
kind
ofType {
name
kind
}
}
}
}
}Implementace v Mku je prakticky stejná jako v příkladu s proměnnými. Hodnotu true nebo false si uložíš jako proměnnou a pak si GraphQL dotaz postavíš jako funkci, která tenhle parametr používá.
Pagination
Rick & Morty API přklad
A nakonec stránkování. Stejně jako u REST API ti pomůže rozdělit výsledky do menších částí zvaných stránky. Typ stránkování záleží na API, může být založené na kurzorech offsetech nebo na něčem vlastním.
Implementace v Mku je skoro stejná jako u REST API příkladu, který jsem zmiňoval dřív na blogu. U Rick a Morty API máme stránkování celkem přímočaré, API vrací pole next. Jakmile next vrátí null, víš, že jsi na poslední stránce. K dispozici je navíc i celkový počet stránek v poli pages, takže si můžeš teoreticky vygenerovat seznam od jedné do čtyřiceti dvou a jet podle toho. Ale na to bych se nespoléhal.
Pokud tě zajímá přehled hlavních typů stránkování v REST API, mrkni na tenhle článek.
První může být trochu složitější na implementaci, takže ti ji ukážu.
Tady je náš GraphQL dotaz:
query getCharactersWithPaging($pageNumber: Int) {
characters(page: $pageNumber) {
info {
next
pages
}
results {
id
name
status
}
}
}Tenhle dotaz budeme volat pořád dokola s proměnnou pageNumber, která se vždy nastaví podle hodnoty z pole next. Jakmile pole next vrátí null víme, že jsme na poslední stránce a můžeme přestat.
Možná tě napadne použít direktivu a pole info načítat jen pro debug a pak ho zase vypnout. Zní to hezky, ale ve skutečnosti by to celý cyklus rozbilo. Jakmile odstraníš pole, podle kterého kontroluješ, jestli pokračovat, celý proces přestane fungovat. Takže pozor na to.
let
graphQLQuery = "
query getCharactersWithPaging($pageNumber: Int) {
characters(page: $pageNumber) {
info {
next
pages
}
results {
id
name
status
}
}
}",
requestFunction = (query as text, page as nullable number) =>
let
requestRecord = [
query = query,
variables = [ pageNumber = page ]
],
requestBody = Json.FromValue(requestRecord),
response =
Json.Document(
Web.Contents(
"https://rickandmortyapi.com",
[
RelativePath = "graphql",
Headers = [ #"Content-Type" = "application/json" ],
Content = requestBody
]
)
)
in
response,
callCharacters =
List.Generate(
() => [
request = requestFunction(graphQLQuery, null),
nextPage = request[data]?[characters]?[info]?[next]?
],
each [nextPage]? <> null,
each [
request = requestFunction(graphQLQuery, nextPage),
nextPage = [request]?[data]?[characters]?[info]?[next]?
],
each [request]?[data]?[characters]?[results]?
),
combineResults = List.Combine(callCharacters),
buildTable =
#table(
type table [
id = Int64.Type,
name = Text.Type,
status = Text.Type
],
List.Transform(
combineResults,
each {
Number.From([id]?),
[name]?,
[status]?
}
)
)
in
buildTableTady není nic zákeřného nebo složitého. Funguje to stejně jako příklad s nextPage v mém jiném článku. Protože List.Generate je vždy o jednu iteraci pozadu; okážeme zachytit okamžik, kdy nextPage přepne na null a pořád stáhnout data z poslední stránky. Jediná věc, na kterou si dát pozor, je počáteční hodnota. To API totiž jako výchozí stránku akceptuje 0, 1 nebo null.
Podle pole prev na stránce 2 je to hodnota 1.
Výsledkem je seznam seznamů. Ten pak spojíme do jednoho seznamu záznamů a pomocí konstruktoru #table vytvoříme tabulku s jasně definovanými typy. Bylo by fajn mít i automatický konstruktor, protože dotaz už má typy nadefinované.
Experimentování s automatickým vytvářením typovaných tabulek z GraphQL
Rick & Morty API přklad
Disclaimer: Tahle část ukazuje, jak automaticky vytvořit typovanou tabulku přímo z GraphQL API pomocí seznamu polí. Příklad je zjednodušený a přizpůsobený Rick & Morty API, ale základní princip platí obecně. Když budeš tvořit jakýkoliv automatický generátor schématu, zvaž, jestli to pro tvůj případ opravdu dává smysl. Pokud je schéma složité nebo obsahuje spoustu speciálních podmínek, může být extra režie moc náročná.
Jednou z největších výhod GraphQL je, že schéma definujeme dopředu, takže přesně víme, co dostaneme. A protože to víme, můžeme si taky automaticky načíst typy vrácených polí.
Když pracujeme s relativně jednoduchým schématem, kde jsou primitivní typy, stačí nám jen definovat pole, která chceme, a předat je do funkce, která vrátí tabulku s explicitně definovanými typy.
Ukážu ti velmi základní příklad s použitím pouze primitivních typů a jak bys to mohl využít. Malá poznámka: dělám tu pár zkratek. Pracuju jen s skalárními poli a nebudu dělat žádné přejmenování nebo transformace. Pole vezmu tak, jak jsou. Pokud budeš potřebovat něco přejmenovat nebo upravit, budeš muset trochu upravit logiku v konstruktoru #table.
Automatický Type Record v M
Když chceme postavit tabulku pomocí konstruktoru #table s explicitně definovanými typy, musíme dynamicky vytvořit část, která řeší typy sloupců. Protože náš zdroj je GraphQL a to podporuje introspekční dotazy pro získání informací o typech, využijeme toho naplno.
Nejdřív si určíme, co chceme vytáhnout. V tomhle příkladu používám typ "Character". Z něj si vezmeme pole id, name, a status a získáme jejich typy. Jakmile je máme, musíme GraphQL typy přeložit do M typů, protože nejsou 1:1. V rámci funkce mám překladový záznam, který mapování obstará. Pak vyfiltrujeme jen pole, která nás zajímají, a postavíme Type Record.
Pokud ho chceme použít v #table, musí mít Type Record konkrétní vlastnosti: Name, Type, a volitelně Optional. Protože chceme, aby všechna pole byla povinná, můžeme zatím ignorovat pole Optional a nechat všechna pole jako povinná.
let
getTypeSchema = (typeName as text, filterFields as list) =>
let
schemaQuery = "
query getSchema($typeName: String!) {
__type(name: $typeName) {
name
fields {
name
type {
name
kind
ofType {
name
kind
}
}
}
}
}",
requestRecord = [
query = schemaQuery,
variables = [ typeName = typeName ]
],
requestBody = Json.FromValue(requestRecord),
response =
Json.Document(
Web.Contents(
"https://rickandmortyapi.com",
[
RelativePath = "graphql",
Headers = [ #"Content-Type" = "application/json" ],
Content = requestBody
]
)
),
typeTranslation = [
Int = Int64.Type,
Float = type number,
String = type text,
ID = type text,
Boolean = type logical
],
getToFieldsAndFilter =
List.Select(
response[data]?[__type]?[fields]?,
each List.Contains(filterFields, _[name])
),
buildTypeList =
List.Transform(
getToFieldsAndFilter,
each [
Name = [name],
Type = Record.FieldOrDefault(typeTranslation, [type]?[name], type any),
Optional = false
]
),
buildTypeRecord =
Type.ForRecord(
Record.FromList(buildTypeList, filterFields),
false
)
in
buildTypeRecord
in
getTypeSchemaKroky pro dynamické vytváření typů:
Definuj záznam typeTranslation mapující názvy skalárních typů (Int, String, atd.) do Mkových typů (Int64.Type, type text, atd.).
Po získání response[data][__type][fields], vyfiltruj zvolené názvy polí přes List.Select.
Pro každé pole, použij Record.FieldOrDefault(typeTranslation, fieldTypeName, type any), abys určil jeho M typ (výchozí je type any).
Sestav z toho seznam záznamů s poli Name, Type, a Optional = false.
Zavolej Record.FromList a Type.ForRecord na tento seznam a vytvoř finální silně typovaný M záznam.
Pamatuj, že to není úplně univerzální řešení. Pokud pracuješ s ne-primitivními typy jako objekty, seznamy nebo vlastními skaláry, budeš potřebovat další logiku pro jejich rozbalení a zpracování. Ale základní princip tahat typy přes introspekci a mapovat je na M typy platí a dá se podle potřeby rozšířit.
Vytvoření automatické, explicitně typované tabulky v M
Teď už stačí zavolat naši funkci, předat jí seznam požadovaných polí a sestavit tabulku. V tomhle kroku můžeme znovu použít stránkovaný dotaz z předchozí části. Jediný manuální krok je vybrat pole, která chceme z typu Character (v tomhle příkladu staticky). Jakmile to máme, předáme seznam polí jak funkci pro načtení schématu, tak funkci pro samotný dotaz a je hotovo.
Posledním krokem je přepsat konstruktor #table, aby už schéma a typy nebyly definované ručně. Místo toho použijeme naši funkci, která automaticky vygeneruje typy sloupců i řádky.
Jen připomínka: tahle metoda počítá s tím, že pole použiješ přesně tak, jak jsou, bez přejmenování nebo úprav. To je sice značné omezení, ale jde hlavně o představení základní myšlenky. Jakmile ji máš, můžeš si vše podle potřeby upravit a přizpůsobit.
let
characterFields = { "id", "name", "status" },
characterFieldsFragment =
"fragment charFields on Character {" &
Text.Combine(characterFields, " ") &
"}",
tableSchema = getTypeSchema("Character", characterFields),
getCharactersQuery =
"
query getCharactersWithPaging($pageNumber: Int) {
characters(page: $pageNumber) {
info {
next
pages
}
results {
...charFields
}
}
}
" & characterFieldsFragment,
requestFunction = (query as text, page as nullable number) =>
let
requestRecord = [
query = query,
variables = [ pageNumber = page ]
],
requestBody = Json.FromValue(requestRecord),
response =
Json.Document(
Web.Contents(
"https://rickandmortyapi.com",
[
RelativePath = "graphql",
Headers = [ #"Content-Type" = "application/json" ],
Content = requestBody
]
)
)
in
response,
callCharacters =
List.Generate(
() => [
request = requestFunction(getCharactersQuery, null),
nextPage = request[data]?[characters]?[info]?[next]?
],
each [nextPage]? <> null,
each [
request = requestFunction(getCharactersQuery, nextPage),
nextPage = [request]?[data]?[characters]?[info]?[next]?
],
each [request]?[data]?[characters]?[results]?
),
combineResults = List.Combine(callCharacters),
prepareRows =
List.Transform(
combineResults,
(r) =>
List.Transform(
characterFields,
(f) => Record.FieldOrDefault(r, f, null)
)
),
buildTable =
#table(
type table tableSchema,
prepareRows
)
in
buildTableZměny oproti originální stránkovací verzi:
Schéma se tvoří dynamicky pomocí funkce getTypeSchema.
Názvy polí jsou uložené v seznamu characterFields a znovu použité.
Řádky se vytváří pomocí prepareRows, která prochází characterFields.
GraphQL dotaz používá fragment postavený z characterFieldsFragment.
Žádné vlastní převody; hodnoty se tahají přímo pomocí Record.FieldOrDefault.
Vynucení konverze typů u řádků
To, že si postavíš explicitně typované schéma, ještě neznamená, že se hodnoty v řádcích automaticky převedou. A to celé pak trochu ztrácí smysl. Funguje to dobře u textů, ale u čísel nebo booleanů to může být dost ošemetné. Power Query totiž jasně rozlišuje mezi přiřazením typu (přidáním metadat) a skutečnou konverzí hodnoty (jejím přetvořením). Když deklaruješ schéma, vlastně M jen říkáš, jaká data by měla být, ale neověřuje ani nemění jejich hodnoty.
Konverze bys mohl dělat až po vytvoření tabulky, ale lepší je to řešit přímo při tvorbě řádků. Tím držíš všechno pohromadě v konstruktoru tabulky a vyhneš se nutnosti dodatečných transformací.
Pro opravu můžeme upravit typeTranslation tak, aby kromě M typu obsahoval i jednoduchou konverzní funkci, kterou použijeme při vytváření řádků.
Takhle upravíš funkci getTypeSchema (vše ostatní zůstává stejné):
scalarMap = [
Int = [ Type = Int64.Type, Converter = Number.From ],
Float = [ Type = type number, Converter = Number.From ],
String = [ Type = type text, Converter = Text.From ],
ID = [ Type = Int64.Type, Converter = Number.From ],
Boolean = [ Type = type logical, Converter = Logical.From ]
],
defaultEntry = [ Type = type any, Converter = (x) => x ],
getToFieldsAndFilter =
List.Select(
response[data]?[__type]?[fields]?,
each List.Contains(filterFields, [name])
),
buildTypeList =
List.Transform(
getToFieldsAndFilter,
each let
base = [type]?[name],
entry = Record.FieldOrDefault(scalarMap, base, defaultEntry)
in
[
Name = [name],
Type = entry[Type],
Converter = entry[Converter],
Optional = false
]
),
typeRecs = List.Transform(buildTypeList, each [ Type = [Type], Optional = [Optional] ]),
tableType = Type.ForRecord( Record.FromList(typeRecs, filterFields), false )
in
[ TableType = tableType, FieldDefs = buildTypeList ]Co jsme udělali: přejmenovali jsme náš záznam typeTranslation na scalarMap, a pro testovací účely jsem dočasně změnil typ ID na Int, aby byl rozdíl v tabulce jasně vidět. Pak jsme každý záznam v mapě rozšířili o pole Convertor field, společně s Type.Výsledkem je, že funkce teď vrací dvě věci: první je tableType, který obsahuje definici schématu (stejně jako dřív), a druhý je FieldDefs, seznam polí spolu s přiřazenými konverzními funkcemi.
V samotném dotazu, kde tvoříme tabulku, se změní jen pár věcí. Protože funkce getTypeSchema teď vrací dva záznamy, zavoláme ji jednou a výsledky uložíme do samostatných proměnných pro jednodušší přístup:
schemaInfo = getTypeSchema("Character", characterFields),
tableSchema = schemaInfo[TableType],
fieldDefs = schemaInfo[FieldDefs],Proměnnou tableSchema jsem nechal beze změny, takže tam není potřeba nic upravovat. Pro přípravu řádků jsme mírně pozměnili původní logiku, aby aplikovala konverzní funkce. Přidal jsem také základní ošetření chyb: pokud konverze selže, vrátíme původní, neupravenou hodnotu.
prepareRows =
List.Transform(
combineResults,
(r) =>
List.Transform(
fieldDefs,
(def) =>
let
raw = Record.FieldOrDefault(r, def[Name], null)
in
try def[Converter](raw)
otherwise raw
)
),
buildTable =
#table(
type table tableSchema,
prepareRows
)Možná sis všiml téhle části: def[Converter](raw). Tahle syntaxe volá funkci, která je uložená ve vlastním poli záznamu. Protože jsme si naše konvertory uložili jako skutečné funkce v poli Converter, můžeme je takhle dynamicky volat.



super, ještě kdyby tak umělo api v power query automatický update dat :)