Schema-tutorial
Schema-tutorialLes 23: Een API gateway maken

Les 23: Een API gateway maken

Een API gateway is een component in onze applicatie die zorgt voor gecentraliseerde afhandeling van API-communicatie tussen de client en de diverse vereiste services.

De API gateway kan worden geïmplementeerd via GraphQL Persisted Queries die op de server zijn opgeslagen en door de client worden aangeroepen. Deze queries communiceren met een of meer backend-services, verzamelen de resultaten en leveren ze in één antwoord terug aan de client.

Dit zijn enkele voordelen van het gebruik van GraphQL Persisted Queries om een API gateway te bieden:

  • Clients hoeven geen verbindingen met backend-services af te handelen, wat hun logica vereenvoudigt
  • De toegang tot backend-services is gecentraliseerd
  • Er worden geen credentials blootgesteld aan de client
  • De reactie van de service kan worden omgezet naar wat de client verwacht of beter kan verwerken
  • Als een backend-service wordt bijgewerkt, kan de Persisted Query worden aangepast zonder breaking changes bij de client te veroorzaken
  • De server kan logs bijhouden van de toegang tot backend-services en metrics extraheren om de analytics te verbeteren

Deze tutorial-les demonstreert een API gateway die de nieuwste artefacten ophaalt uit de GitHub Actions API en hun URL extraheert om te downloaden, zonder dat de client ingelogd hoeft te zijn bij GitHub.

GraphQL-gestuurde API gateway voor toegang tot GitHub Actions-artefacten

De GraphQL-query hieronder moet worden opgeslagen als een Persisted Query (bijvoorbeeld met de slug retrieve-public-urls-for-github-actions-artifacts).

Het haalt de publiek toegankelijke download-URLs op voor GitHub Actions-artefacten:

  • Het haalt eerst de laatste X artefacten op uit GitHub Actions en extraheert de proxy-URL om elk van hen te benaderen. (Omdat alleen geauthenticeerde gebruikers toegang hebben tot de artefacten, verwijzen deze URLs nog niet naar het eigenlijke artefact.)
  • Vervolgens benadert het elk van deze proxy-URLs (waar het artefact voor een korte periode op een publieke locatie is geüpload) en extraheert de eigenlijke URL uit de Location-header van het HTTP-antwoord
  • Ten slotte worden alle publiek toegankelijke URLs weergegeven, zodat niet-geauthenticeerde gebruikers GitHub-artefacten kunnen downloaden binnen dat tijdsvenster

(De tutorial-les eindigt hier, maar als vervolg zou de GraphQL-query dan iets kunnen doen met deze URLs: ze per e-mail versturen, de bestanden via FTP ergens uploaden, ze installeren op een InstaWP-site, enzovoort.)

query RetrieveGitHubAccessToken {
  githubAccessToken: _env(name: "GITHUB_ACCESS_TOKEN")
    @export(as: "githubAccessToken")
    @remove
}
 
query RetrieveProxyArtifactDownloadURLs($numberArtifacts: Int! = 3)
  @depends(on: "RetrieveGitHubAccessToken")
{
  githubArtifactsEndpoint: _sprintf(
    string: "https://api.github.com/repos/GatoGraphQL/GatoGraphQL/actions/artifacts?per_page=%s",
    values: [$numberArtifacts]
  )
    @remove
 
  # Retrieve Artifact data from GitHub Actions API
  gitHubArtifactData: _sendJSONObjectItemHTTPRequest(
    input: {
      url: $__githubArtifactsEndpoint,
      options: {
        auth: {
          password: $githubAccessToken
        },
        headers: [
          {
            name: "Accept",
            value: "application/vnd.github+json"
          }
        ]
      }
    }
  )
    @remove
  
  # Extract the URL from within each "artifacts" item
  gitHubProxyArtifactDownloadURLs: _objectProperty(
    object: $__gitHubArtifactData,
    by: {
      key: "artifacts"
    }
  )
    @underEachArrayItem(passValueOnwardsAs: "artifactItem")
      @applyField(
        name: "_objectProperty",
        arguments: {
          object: $artifactItem,
          by: {
            key: "archive_download_url"
          }
        },
        setResultInResponse: true
      )
    @export(as: "gitHubProxyArtifactDownloadURLs")
}
 
query CreateHTTPRequestInputs
  @depends(on: "RetrieveProxyArtifactDownloadURLs")
{
  httpRequestInputs: _echo(value: $gitHubProxyArtifactDownloadURLs)
    @underEachArrayItem(
      passValueOnwardsAs: "url"
    )
      @applyField(
        name: "_objectAddEntry",
        arguments: {
          object: {
            options: {
              auth: {
                password: $githubAccessToken
              },
              headers: {
                name: "Accept",
                value: "application/vnd.github+json"
              },
              allowRedirects: null
            }
          },
          key: "url",
          value: $url
        },
        setResultInResponse: true
      )
    @export(as: "httpRequestInputs")
    @remove
}
 
query RetrieveActualArtifactDownloadURLs
  @depends(on: "CreateHTTPRequestInputs")
{
  _sendHTTPRequests(
    inputs: $httpRequestInputs
  ) {
    artifactDownloadURL: header(name: "Location")
      @export(as: "artifactDownloadURLs", type: LIST)
  }
}
 
query PrintArtifactDownloadURLsAsList
  @depends(on: "RetrieveActualArtifactDownloadURLs")
{
  artifactDownloadURLs: _echo(value: $artifactDownloadURLs)
}

Het antwoord is:

{
  "data": {
    "gitHubProxyArtifactDownloadURLs": [
      "https://api.github.com/repos/GatoGraphQL/GatoGraphQL/actions/artifacts/803444209/zip",
      "https://api.github.com/repos/GatoGraphQL/GatoGraphQL/actions/artifacts/803444208/zip",
      "https://api.github.com/repos/GatoGraphQL/GatoGraphQL/actions/artifacts/803444207/zip"
    ],
    "_sendHTTPRequests": [
      {
        "artifactDownloadURL": "https://pipelines.actions.githubusercontent.com/serviceHosts/a6be3ecc-6518-4aaa-b5ec-232be0438a37/_apis/pipelines/1/runs/53473/signedartifactscontent?artifactName=gato-graphql-testing-schema-1.0.0-dev&urlExpires=2023-07-14T03%3A31%3A00.9351393Z&urlSigningMethod=HMACV2&urlSignature=8v8cDVZKAnkXoN8z1GdjXLz4SCGkpv%2Fl0qjlDArac5M%3D"
      },
      {
        "artifactDownloadURL": "https://pipelines.actions.githubusercontent.com/serviceHosts/a6be3ecc-6518-4aaa-b5ec-232be0438a37/_apis/pipelines/1/runs/53473/signedartifactscontent?artifactName=gato-graphql-testing-1.0.0-dev&urlExpires=2023-07-14T03%3A31%3A00.9333471Z&urlSigningMethod=HMACV2&urlSignature=ffsyy0p97oeQByMD3X6WKbFyIEbh6nbU%2BFsXKHQHYSM%3D"
      },
      {
        "artifactDownloadURL": "https://pipelines.actions.githubusercontent.com/serviceHosts/a6be3ecc-6518-4aaa-b5ec-232be0438a37/_apis/pipelines/1/runs/53473/signedartifactscontent?artifactName=gato-graphql-1.0.0-dev&urlExpires=2023-07-14T03%3A31%3A00.9699160Z&urlSigningMethod=HMACV2&urlSignature=gUi%2F39RS7X5YgVZbEu977ufFt1girQKeNI7LP61gxfY%3D"
      }
    ],
    "artifactDownloadURLs": [
      "https://pipelines.actions.githubusercontent.com/serviceHosts/a6be3ecc-6518-4aaa-b5ec-232be0438a37/_apis/pipelines/1/runs/53473/signedartifactscontent?artifactName=gato-graphql-testing-schema-1.0.0-dev&urlExpires=2023-07-14T03%3A31%3A00.9351393Z&urlSigningMethod=HMACV2&urlSignature=8v8cDVZKAnkXoN8z1GdjXLz4SCGkpv%2Fl0qjlDArac5M%3D",
      "https://pipelines.actions.githubusercontent.com/serviceHosts/a6be3ecc-6518-4aaa-b5ec-232be0438a37/_apis/pipelines/1/runs/53473/signedartifactscontent?artifactName=gato-graphql-testing-1.0.0-dev&urlExpires=2023-07-14T03%3A31%3A00.9333471Z&urlSigningMethod=HMACV2&urlSignature=ffsyy0p97oeQByMD3X6WKbFyIEbh6nbU%2BFsXKHQHYSM%3D",
      "https://pipelines.actions.githubusercontent.com/serviceHosts/a6be3ecc-6518-4aaa-b5ec-232be0438a37/_apis/pipelines/1/runs/53473/signedartifactscontent?artifactName=gato-graphql-1.0.0-dev&urlExpires=2023-07-14T03%3A31%3A00.9699160Z&urlSigningMethod=HMACV2&urlSignature=gUi%2F39RS7X5YgVZbEu977ufFt1girQKeNI7LP61gxfY%3D"
    ]
  }
}

Alternatief: de GitHub-credentials ophalen uit het HTTP-verzoek

We kunnen onze gebruikers ook toestaan hun eigen GitHub-credentials via een header mee te sturen.

Deze GraphQL-query is een aanpassing van de vorige, met de volgende verschillen:

  • Operatie RetrieveGitHubAccessToken leest en exporteert de waarde uit de X-Github-Access-Token-header van het huidige HTTP-verzoek, en geeft aan of deze header niet is meegegeven
  • FailIfGitHubAccessTokenIsMissing activeert een fout wanneer de header ontbreekt
  • Aan alle andere operaties is de directive @skip(if: $isGithubAccessTokenMissing) toegevoegd, zodat ze niet worden uitgevoerd als het token ontbreekt
query RetrieveGitHubAccessToken {
  githubAccessToken: _httpRequestHeader(name: "X-Github-Access-Token")
    @export(as: "githubAccessToken")
    @remove
 
  isGithubAccessTokenMissing: _isEmpty(value: $__githubAccessToken)
    @export(as: "isGithubAccessTokenMissing")
}
 
query FailIfGitHubAccessTokenIsMissing
  @depends(on: "RetrieveGitHubAccessToken")
  @include(if: $isGithubAccessTokenMissing)
{
  _fail(
    message: "Header 'X-Github-Access-Token' has not been provided"
  ) @remove
}
 
query RetrieveProxyArtifactDownloadURLs($numberArtifacts: Int! = 3)
  @depends(on: "RetrieveGitHubAccessToken")
  @skip(if: $isGithubAccessTokenMissing)
{
  # Do same as before
  # ...
}
 
query CreateHTTPRequestInputs
  @depends(on: "RetrieveProxyArtifactDownloadURLs")
  @skip(if: $isGithubAccessTokenMissing)
{
  # Do same as before
  # ...
}
 
query RetrieveActualArtifactDownloadURLs
  @depends(on: "CreateHTTPRequestInputs")
  @skip(if: $isGithubAccessTokenMissing)
{
  # Do same as before
  # ...
}
 
query PrintArtifactDownloadURLsAsList
  @depends(on: [
    "RetrieveActualArtifactDownloadURLs",
    "FailIfGitHubAccessTokenIsMissing"
  ])
  @skip(if: $isGithubAccessTokenMissing)
{
  # Do same as before
  # ...
}

Wanneer de header X-Github-Access-Token is meegegeven, is het antwoord hetzelfde als hierboven.

Wanneer die niet is meegegeven, zal het antwoord zijn:

{
  "errors": [
    {
      "message": "Header 'X-Github-Access-Token' has not been provided",
      "locations": [
        {
          "line": 18,
          "column": 3
        }
      ],
      "extensions": {
        "path": [
          "_fail(message: \"Header 'X-Github-Access-Token' has not been provided\") @remove",
          "query FailIfGitHubAccessTokenIsMissing @depends(on: \"ValidateHasGitHubAccessToken\") @skip(if: $isGithubAccessTokenMissing) { ... }"
        ],
        "type": "QueryRoot",
        "field": "_fail(message: \"Header 'X-Github-Access-Token' has not been provided\") @remove",
        "id": "root",
        "code": "PoPSchema/FailFieldAndDirective@e1"
      }
    }
  ],
  "data": {
    "isGithubAccessTokenMissing": false
  }
}

We kunnen via headers de credentials ophalen voor meerdere services die in de API gateway worden gebruikt, terwijl we valideren dat ze allemaal zijn meegegeven:

query RetrieveServiceTokens {
  githubAccessToken: _httpRequestHeader(name: "X-Github-Access-Token")
    @export(as: "githubAccessToken")
  slackAccessToken: _httpRequestHeader(name: "X-Slack-Access-Token")
    @export(as: "slackAccessToken")
 
  isGithubAccessTokenMissing: _isEmpty(value: $__githubAccessToken)
  isSlackAccessTokenMissing: _isEmpty(value: $__slackAccessToken)    
  isAnyAccessTokenMissing: _or(values: [
    $__isGithubAccessTokenMissing,
    $__isSlackAccessTokenMissing
  ])
    @export(as: "isAnyAccessTokenMissing")
}
 
query FailIfAnyAccessTokenMissing
  @depends(on: "RetrieveServiceTokens")
  @include(if: $isAnyAccessTokenMissing)
{
  _fail(
    message: "Access tokens for GitHub and Slack must be provided"
  ) @remove
}
 
query RetrieveProxyArtifactDownloadURLs
  @depends(on: "RetrieveServiceTokens")
  @skip(if: $isAnyAccessTokenMissing)
{
  # Do something
  # ...
}
 
# Do something
# ...

Stap voor stap: de GraphQL-query opbouwen

Hieronder volgt een gedetailleerde analyse van hoe de query werkt.

Het endpoint waarmee verbinding wordt gemaakt, kan dynamisch worden gegenereerd, in dit geval met _sprintf:

query RetrieveProxyArtifactDownloadURLs($numberArtifacts: Int! = 3)
  @depends(on: "RetrieveGitHubAccessToken")
{
  githubArtifactsEndpoint: _sprintf(
    string: "https://api.github.com/repos/GatoGraphQL/GatoGraphQL/actions/artifacts?per_page=%s",
    values: [$numberArtifacts]
  )
    @remove
 
  # ...
}

Het antwoord van de GitHub Actions API is omvangrijk en voor ons niet interessant, dus gebruiken we @remove om het uit het antwoord te verwijderen. Tijdens de ontwikkeling schakelen we deze directive echter uit, zodat we de structuur van het teruggegeven JSON-object kunnen visualiseren en begrijpen, en de gegevenselementen kunnen identificeren die we nodig hebben:

query RetrieveProxyArtifactDownloadURLs($numberArtifacts: Int! = 3)
  @depends(on: "RetrieveGitHubAccessToken")
{
  # ...
 
  # Retrieve Artifact data from GitHub Actions API
  gitHubArtifactData: _sendJSONObjectItemHTTPRequest(
    input: {
      url: $__githubArtifactsEndpoint,
      options: {
        auth: {
          password: $githubAccessToken
        },
        headers: [
          {
            name: "Accept",
            value: "application/vnd.github+json"
          }
        ]
      }
    }
  )
    # @remove   <= Disabled to visualize output
}

Het antwoord is:

{
  "data": {
    "gitHubArtifactData": {
      "total_count": 8344,
      "artifacts": [
        {
          "id": 803739808,
          "node_id": "MDg6QXJ0aWZhY3Q4MDM3Mzk4MDg=",
          "name": "gato-graphql-testing-schema-1.0.0-dev",
          "size_in_bytes": 62952,
          "url": "https://api.github.com/repos/GatoGraphQL/GatoGraphQL/actions/artifacts/803739808",
          "archive_download_url": "https://api.github.com/repos/GatoGraphQL/GatoGraphQL/actions/artifacts/803739808/zip",
          "expired": false,
          "created_at": "2023-07-14T06:25:57Z",
          "updated_at": "2023-07-14T06:25:59Z",
          "expires_at": "2023-08-13T06:17:15Z",
          "workflow_run": {
            "id": 5551097653,
            "repository_id": 66721227,
            "head_repository_id": 66721227,
            "head_branch": "Enable-headers-in-GraphiQL",
            "head_sha": "31e69ccab2a8d1fdea942e71f7a93ec484bdd9c8"
          }
        },
        {
          "id": 803739806,
          "node_id": "MDg6QXJ0aWZhY3Q4MDM3Mzk4MDY=",
          "name": "gato-graphql-testing-1.0.0-dev",
          "size_in_bytes": 123914,
          "url": "https://api.github.com/repos/GatoGraphQL/GatoGraphQL/actions/artifacts/803739806",
          "archive_download_url": "https://api.github.com/repos/GatoGraphQL/GatoGraphQL/actions/artifacts/803739806/zip",
          "expired": false,
          "created_at": "2023-07-14T06:25:57Z",
          "updated_at": "2023-07-14T06:25:59Z",
          "expires_at": "2023-08-13T06:17:11Z",
          "workflow_run": {
            "id": 5551097653,
            "repository_id": 66721227,
            "head_repository_id": 66721227,
            "head_branch": "Enable-headers-in-GraphiQL",
            "head_sha": "31e69ccab2a8d1fdea942e71f7a93ec484bdd9c8"
          }
        },
        {
          "id": 803739803,
          "node_id": "MDg6QXJ0aWZhY3Q4MDM3Mzk4MDM=",
          "name": "gato-graphql-1.0.0-dev",
          "size_in_bytes": 33394234,
          "url": "https://api.github.com/repos/GatoGraphQL/GatoGraphQL/actions/artifacts/803739803",
          "archive_download_url": "https://api.github.com/repos/GatoGraphQL/GatoGraphQL/actions/artifacts/803739803/zip",
          "expired": false,
          "created_at": "2023-07-14T06:25:57Z",
          "updated_at": "2023-07-14T06:25:59Z",
          "expires_at": "2023-08-13T06:21:42Z",
          "workflow_run": {
            "id": 5551097653,
            "repository_id": 66721227,
            "head_repository_id": 66721227,
            "head_branch": "Enable-headers-in-GraphiQL",
            "head_sha": "31e69ccab2a8d1fdea942e71f7a93ec484bdd9c8"
          }
        }
      ]
    }
  }
}

Het gegevenselement dat ons interesseert is de eigenschap "archive_download_url". We navigeren naar elk van deze gegevenselementen in de JSON-objectstructuur, extraheren die waarde met het veld _objectProperty (toegepast via directive @applyField) en overschrijven het geïtereerde element door het argument setResultInResponse: true mee te geven:

query RetrieveProxyArtifactDownloadURLs($numberArtifacts: Int! = 3)
  @depends(on: "RetrieveGitHubAccessToken")
{
  # ...
  
  # Extract the URL from within each "artifacts" item
  gitHubProxyArtifactDownloadURLs: _objectProperty(
    object: $__gitHubArtifactData,
    by: {
      key: "artifacts"
    }
  )
    @underEachArrayItem(passValueOnwardsAs: "artifactItem")
      @applyField(
        name: "_objectProperty",
        arguments: {
          object: $artifactItem,
          by: {
            key: "archive_download_url"
          }
        },
        setResultInResponse: true
      )
    @export(as: "gitHubProxyArtifactDownloadURLs")
}

We verbinden tegelijkertijd met alle geëxtraheerde artefact-URLs via het veld _sendHTTPRequests (waarmee meerdere HTTP-verzoeken asynchroon worden verstuurd) en bevragen de Location-header van elk antwoord.

Omdat het veld _sendHTTPRequests het argument input ontvangt (van het type [HTTPRequestInput]), genereren we deze input dynamisch door:

  • Elk van de artefact-URLs te itereren (opgeslagen in de dynamische variabele $gitHubProxyArtifactDownloadURLs)
  • Dynamisch een JSON-object te bouwen voor elk van hen (met het veld _objectAddEntry) dat alle vereiste parameters bevat (headers, authenticatie en andere)
  • De URL aan dit JSON-object toe te voegen (beschikbaar in de dynamische variabele $url)

Deze lijst van dynamisch aangemaakte JSON-objecten wordt geconverteerd naar [HTTPRequestInput] wanneer die als argument aan _sendHTTPRequests(input:) wordt meegegeven. Als onze procedure niet correct was en een item niet naar HTTPRequestInput kan worden geconverteerd (bijvoorbeeld omdat we geen verplichte eigenschap hebben opgegeven of een niet-bestaande eigenschap hebben gebruikt), geeft de GraphQL-server een conversie-fout.

Merk op dat we @remove moeten toepassen op het veld httpRequestInputs, omdat het de GitHub-token bevat (via password: $githubAccessToken), die we niet in het antwoord willen tonen. Tijdens de ontwikkeling kunnen we deze directive echter uitschakelen.

query CreateHTTPRequestInputs
  @depends(on: "RetrieveProxyArtifactDownloadURLs")
{
  httpRequestInputs: _echo(value: $gitHubProxyArtifactDownloadURLs)
    @underEachArrayItem(
      passValueOnwardsAs: "url"
    )
      @applyField(
        name: "_objectAddEntry",
        arguments: {
          object: {
            options: {
              auth: {
                password: $githubAccessToken
              },
              headers: {
                name: "Accept",
                value: "application/vnd.github+json"
              },
              allowRedirects: null
            }
          },
          key: "url",
          value: $url
        },
        setResultInResponse: true
      )
    @export(as: "httpRequestInputs")
    # @remove   <= Disabled to visualize output
}
 
query RetrieveActualArtifactDownloadURLs
  @depends(on: "CreateHTTPRequestInputs")
{
  _sendHTTPRequests(
    inputs: $httpRequestInputs
  ) {
    artifactDownloadURL: header(name: "Location")
      @export(as: "artifactDownloadURLs", type: LIST)
  }
}

Omdat @remove nu uitgecommentarieerd is, kunnen we de gegenereerde JSON-objectinputs in het antwoord visualiseren (onder het item httpRequestInputs), en vervolgens de resulterende Location-header van elk HTTP-antwoord (onder alias artifactDownloadURL):

{
  "data": {
    "gitHubProxyArtifactDownloadURLs": [
      // ...
    ],
    "httpRequestInputs": [
      {
        "options": {
          "auth": {
            "password": "ghp_{some_github_access_token}"
          },
          "headers": {
            "name": "Accept",
            "value": "application/vnd.github+json"
          },
          "allowRedirects": null
        },
        "url": "https://api.github.com/repos/GatoGraphQL/GatoGraphQL/actions/artifacts/803739808/zip"
      },
      {
        "options": {
          "auth": {
            "password": "ghp_{some_github_access_token}"
          },
          "headers": {
            "name": "Accept",
            "value": "application/vnd.github+json"
          },
          "allowRedirects": null
        },
        "url": "https://api.github.com/repos/GatoGraphQL/GatoGraphQL/actions/artifacts/803739806/zip"
      },
      {
        "options": {
          "auth": {
            "password": "ghp_{some_github_access_token}"
          },
          "headers": {
            "name": "Accept",
            "value": "application/vnd.github+json"
          },
          "allowRedirects": null
        },
        "url": "https://api.github.com/repos/GatoGraphQL/GatoGraphQL/actions/artifacts/803739803/zip"
      }
    ],
    "_sendHTTPRequests": [
      {
        "artifactDownloadURL": "https://pipelines.actions.githubusercontent.com/serviceHosts/a6be3ecc-6518-4aaa-b5ec-232be0438a37/_apis/pipelines/1/runs/53479/signedartifactscontent?artifactName=gato-graphql-testing-schema-1.0.0-dev&urlExpires=2023-07-14T07%3A26%3A47.2766840Z&urlSigningMethod=HMACV2&urlSignature=Ype82npdlUlLk4gcGZcBiz80e0ZuvcvnC2rdaSDg9p8%3D"
      },
      {
        "artifactDownloadURL": "https://pipelines.actions.githubusercontent.com/serviceHosts/a6be3ecc-6518-4aaa-b5ec-232be0438a37/_apis/pipelines/1/runs/53479/signedartifactscontent?artifactName=gato-graphql-testing-1.0.0-dev&urlExpires=2023-07-14T07%3A26%3A47.2961965Z&urlSigningMethod=HMACV2&urlSignature=FdWAh8JXNPJsVIPNuiYN8R7i0vRnN8eCGc57VZDNUEc%3D"
      },
      {
        "artifactDownloadURL": "https://pipelines.actions.githubusercontent.com/serviceHosts/a6be3ecc-6518-4aaa-b5ec-232be0438a37/_apis/pipelines/1/runs/53479/signedartifactscontent?artifactName=gato-graphql-1.0.0-dev&urlExpires=2023-07-14T07%3A26%3A47.2861087Z&urlSigningMethod=HMACV2&urlSignature=0Go8QnkZqIbn0urTQqfbMW4rQtjMfDAR9fSm6fCePjw%3D"
      }
    ]
  }
}

Ten slotte tonen we alle artifactDownloadURL-items samen als een lijst (beschikbaar in de dynamische variabele $artifactDownloadURLs) met _echo:

query PrintArtifactDownloadURLsAsList
  @depends(on: "RetrieveActualArtifactDownloadURLs")
{
  artifactDownloadURLs: _echo(value: $artifactDownloadURLs)
}

Dit geeft het volgende weer:

{
  "data": {
    // ...
    "artifactDownloadURLs": [
      "https://pipelines.actions.githubusercontent.com/serviceHosts/a6be3ecc-6518-4aaa-b5ec-232be0438a37/_apis/pipelines/1/runs/53479/signedartifactscontent?artifactName=gato-graphql-testing-schema-1.0.0-dev&urlExpires=2023-07-14T07%3A37%3A42.4998268Z&urlSigningMethod=HMACV2&urlSignature=1c1qNRfD9KFwSuzMjw9tsumq9B5I1c9H4LWgSbR0Kwg%3D",
      "https://pipelines.actions.githubusercontent.com/serviceHosts/a6be3ecc-6518-4aaa-b5ec-232be0438a37/_apis/pipelines/1/runs/53479/signedartifactscontent?artifactName=gato-graphql-testing-1.0.0-dev&urlExpires=2023-07-14T07%3A37%3A42.4878741Z&urlSigningMethod=HMACV2&urlSignature=htjc1HrmZpbecECpBQnEHhlP7lkqkdyjzATb0vFnzDE%3D",
      "https://pipelines.actions.githubusercontent.com/serviceHosts/a6be3ecc-6518-4aaa-b5ec-232be0438a37/_apis/pipelines/1/runs/53479/signedartifactscontent?artifactName=gato-graphql-1.0.0-dev&urlExpires=2023-07-14T07%3A37%3A42.5240496Z&urlSigningMethod=HMACV2&urlSignature=YDuHFqweL9m6LIycLsVy0bJJ4zePc4pWkHz8RfjfzCg%3D"
    ]
  }
}