Schema-Tutorial
Schema-TutorialLektion 23: Ein API-Gateway erstellen

Lektion 23: Ein API-Gateway erstellen

Ein API-Gateway ist eine Komponente unserer Anwendung, die die API-Kommunikation zwischen dem Client und den verschiedenen erforderlichen Diensten zentral verwaltet.

Das API-Gateway kann über GraphQL Persisted Queries implementiert werden, die auf dem Server gespeichert und vom Client aufgerufen werden. Diese interagieren mit einem oder mehreren Backend-Diensten, sammeln die Ergebnisse und liefern sie in einer einzigen Antwort an den Client zurück.

Hier sind einige Vorteile der Verwendung von GraphQL Persisted Queries für ein API-Gateway:

  • Clients müssen keine Verbindungen zu Backend-Diensten verwalten, was ihre Logik vereinfacht
  • Der Zugriff auf Backend-Dienste ist zentralisiert
  • Es werden keine Zugangsdaten auf dem Client offengelegt
  • Die Antwort des Dienstes kann in das umgewandelt werden, was der Client erwartet oder besser verarbeiten kann
  • Wenn ein Backend-Dienst aktualisiert wird, kann die Persisted Query angepasst werden, ohne Breaking Changes auf der Client-Seite zu verursachen
  • Der Server kann Zugriffsprotokolle für die Backend-Dienste speichern und Metriken extrahieren, um die Analyse zu verbessern

Diese Tutorial-Lektion zeigt ein API-Gateway, das die neuesten Artefakte aus der GitHub Actions API abruft und ihre Download-URL extrahiert, sodass der Client nicht bei GitHub angemeldet sein muss.

GraphQL-basiertes API-Gateway für den Zugriff auf GitHub Actions-Artefakte

Die folgende GraphQL-Abfrage muss als Persisted Query gespeichert werden (z. B. mit dem Slug retrieve-public-urls-for-github-actions-artifacts).

Sie ruft die öffentlich zugänglichen Download-URLs für GitHub Actions-Artefakte ab:

  • Zunächst werden die neuesten X Artefakte von GitHub Actions abgerufen und die Proxy-URL für den Zugriff auf jedes davon extrahiert. (Da nur authentifizierte Benutzer auf die Artefakte zugreifen können, verweisen diese URLs noch nicht auf das eigentliche Artefakt.)
  • Dann wird auf jede dieser Proxy-URLs zugegriffen (das Artefakt wurde für kurze Zeit an einem öffentlichen Ort hochgeladen) und die tatsächliche URL aus dem Location-Header der HTTP-Antwort extrahiert
  • Schließlich werden alle öffentlich zugänglichen URLs ausgegeben, damit nicht authentifizierte Benutzer GitHub-Artefakte innerhalb dieses Zeitfensters herunterladen können

(Die Tutorial-Lektion endet hier, aber als Fortsetzung könnte die GraphQL-Abfrage dann etwas mit diesen URLs tun: sie per E-Mail versenden, die Dateien per FTP irgendwo hochladen, sie auf einer InstaWP-Website installieren usw.)

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)
}

Die Antwort lautet:

{
  "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"
    ]
  }
}

Alternative: GitHub-Zugangsdaten aus der HTTP-Anfrage beziehen

Wir können unseren Benutzern auch ermöglichen, ihre eigenen GitHub-Zugangsdaten per Header bereitzustellen.

Diese GraphQL-Abfrage ist eine Anpassung der vorherigen, mit folgenden Unterschieden:

  • Die Operation RetrieveGitHubAccessToken liest und exportiert den Wert aus dem X-Github-Access-Token-Header der aktuellen HTTP-Anfrage und gibt an, ob dieser Header nicht bereitgestellt wurde
  • FailIfGitHubAccessTokenIsMissing löst einen Fehler aus, wenn der Header fehlt
  • Allen anderen Operationen wurde die Direktive @skip(if: $isGithubAccessTokenMissing) hinzugefügt, sodass sie nicht ausgeführt werden, wenn das Token fehlt
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
  # ...
}

Wenn der Header X-Github-Access-Token bereitgestellt wird, ist die Antwort dieselbe wie oben.

Wenn er nicht bereitgestellt wird, lautet die Antwort:

{
  "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
  }
}

Wir können aus den Headern die Zugangsdaten für mehrere im API-Gateway verwendete Dienste abrufen und gleichzeitig prüfen, ob alle bereitgestellt wurden:

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
# ...

Schritt für Schritt: die GraphQL-Abfrage erstellen

Im Folgenden wird die Abfrage im Detail erklärt.

Der Endpunkt, zu dem eine Verbindung hergestellt werden soll, kann dynamisch generiert werden, in diesem Fall mit _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
 
  # ...
}

Die Antwort der GitHub Actions API ist umfangreich und für uns nicht von Interesse, daher entfernen wir sie mit @remove aus der Antwort. Während der Entwicklung deaktivieren wir diese Direktive jedoch, um die Struktur des zurückgegebenen JSON-Objekts zu visualisieren und zu verstehen sowie die Datenelemente zu identifizieren, die wir extrahieren müssen:

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
}

Die Antwort lautet:

{
  "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"
          }
        }
      ]
    }
  }
}

Das Datenelement, das uns interessiert, ist die Eigenschaft "archive_download_url". Wir navigieren zu jedem dieser Datenelemente innerhalb der JSON-Objektstruktur, extrahieren diesen Wert mit dem Feld _objectProperty (angewendet via Direktive @applyField) und überschreiben das iterierte Element, indem wir das Argument setResultInResponse: true übergeben:

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")
}

Wir verbinden uns gleichzeitig mit allen extrahierten Artefakt-URLs über das Feld _sendHTTPRequests (die mehreren HTTP-Anfragen werden asynchron gesendet) und fragen den Location-Header aus jeder Antwort ab.

Da das Feld _sendHTTPRequests das Argument input (vom Typ [HTTPRequestInput]) empfängt, generieren wir diesen Input dynamisch:

  • Indem wir jede der Artefakt-URLs durchlaufen (gespeichert unter der dynamischen Variable $gitHubProxyArtifactDownloadURLs)
  • Indem wir für jede davon dynamisch ein JSON-Objekt erstellen (mit dem Feld _objectAddEntry), das alle erforderlichen Parameter enthält (Header, Authentifizierung und andere)
  • Indem wir die URL an dieses JSON-Objekt anhängen (verfügbar unter der dynamischen Variable $url)

Diese Liste dynamisch erstellter JSON-Objekte wird zu [HTTPRequestInput] umgewandelt, wenn sie als Argument an _sendHTTPRequests(input:) übergeben wird. Falls unser Vorgehen nicht korrekt war und ein Element nicht zu HTTPRequestInput umgewandelt werden kann (z. B. weil wir eine Pflichtangabe nicht gemacht oder eine nicht existierende Eigenschaft angegeben haben), gibt der GraphQL-Server einen Konvertierungsfehler aus.

Beachte, dass wir @remove auf das Feld httpRequestInputs anwenden müssen, da es das GitHub-Token enthält (unter password: $githubAccessToken), das wir nicht in der Antwort ausgeben wollen. Während der Entwicklung können wir diese Direktive jedoch deaktivieren.

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)
  }
}

Da @remove nun auskommentiert ist, können wir die generierten JSON-Objekt-Inputs in der Antwort visualisieren (unter dem Eintrag httpRequestInputs) sowie den resultierenden Location-Header aus jeder HTTP-Antwort (unter dem 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"
      }
    ]
  }
}

Schließlich geben wir alle artifactDownloadURL-Elemente zusammen als Liste aus (verfügbar unter der dynamischen Variable $artifactDownloadURLs), mit _echo:

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

Dies gibt aus:

{
  "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"
    ]
  }
}