บทช่วยสอน Schema
บทช่วยสอน Schemaบทเรียนที่ 23: การสร้าง API gateway

บทเรียนที่ 23: การสร้าง API gateway

API gateway คือคอมโพเนนต์ในแอปพลิเคชันของเราที่ทำหน้าที่จัดการการสื่อสาร API ระหว่างไคลเอนต์กับบริการที่จำเป็นหลายตัวแบบรวมศูนย์

API gateway สามารถนำไปใช้งานผ่าน GraphQL Persisted Queries ที่จัดเก็บไว้บนเซิร์ฟเวอร์และถูกเรียกใช้โดยไคลเอนต์ ซึ่งจะโต้ตอบกับบริการแบ็กเอนด์หนึ่งตัวหรือมากกว่า รวบรวมผลลัพธ์ และส่งกลับไปยังไคลเอนต์ในรูปแบบเรสปอนส์เดียว

ต่อไปนี้คือประโยชน์บางส่วนของการใช้ GraphQL Persisted Queries เพื่อให้บริการ API gateway:

  • ไคลเอนต์ไม่จำเป็นต้องจัดการการเชื่อมต่อกับบริการแบ็กเอนด์ จึงทำให้ลอจิกของไคลเอนต์เรียบง่ายขึ้น
  • การเข้าถึงบริการแบ็กเอนด์ถูกรวมศูนย์
  • ไม่มีข้อมูลรับรองใดถูกเปิดเผยบนฝั่งไคลเอนต์
  • เรสปอนส์จากบริการสามารถแปลงให้อยู่ในรูปแบบที่ไคลเอนต์คาดหวังหรือจัดการได้ดียิ่งขึ้น
  • หากมีการอัปเกรดบริการแบ็กเอนด์ใด ก็สามารถปรับ Persisted Query ได้โดยไม่ทำให้เกิดการเปลี่ยนแปลงที่ทำลายความเข้ากันได้กับไคลเอนต์
  • เซิร์ฟเวอร์สามารถจัดเก็บบันทึกการเข้าถึงบริการแบ็กเอนด์ และแยกข้อมูลเมตริกเพื่อเสริมการวิเคราะห์

บทเรียนนี้สาธิต API gateway ที่ดึงอาร์ติแฟกต์ล่าสุดจาก GitHub Actions API และแยก URL สำหรับดาวน์โหลด โดยไม่จำเป็นต้องให้ไคลเอนต์ลงชื่อเข้าใช้ GitHub

API gateway ที่ขับเคลื่อนด้วย GraphQL เพื่อเข้าถึงอาร์ติแฟกต์ของ GitHub Action

GraphQL query ด้านล่างนี้ต้องจัดเก็บเป็น Persisted Query (เช่น ใช้ slug retrieve-public-urls-for-github-actions-artifacts)

โดยจะดึง URL สำหรับดาวน์โหลดที่เข้าถึงได้แบบสาธารณะของอาร์ติแฟกต์ GitHub Actions:

  • ขั้นแรกจะดึงอาร์ติแฟกต์ล่าสุดจำนวน X รายการจาก GitHub Actions และแยกพร็อกซี URL สำหรับเข้าถึงแต่ละรายการ (เนื่องจากมีเพียงผู้ใช้ที่ผ่านการยืนยันตัวตนเท่านั้นที่เข้าถึงอาร์ติแฟกต์ได้ URL เหล่านี้จึงยังไม่ได้ชี้ไปยังอาร์ติแฟกต์จริง)
  • จากนั้นจะเข้าถึงพร็อกซี URL แต่ละรายการ (ซึ่งมีอาร์ติแฟกต์อัปโหลดไว้ในตำแหน่งสาธารณะเป็นช่วงเวลาสั้น ๆ) และแยก URL จริงจากเฮดเดอร์ Location ของ HTTP เรสปอนส์
  • สุดท้ายจะพิมพ์ URL ที่เข้าถึงได้แบบสาธารณะทั้งหมด ทำให้ผู้ใช้ที่ไม่ได้ผ่านการยืนยันตัวตนสามารถดาวน์โหลดอาร์ติแฟกต์ GitHub ได้ภายในช่วงเวลานั้น

(บทเรียนจบลงที่นี่ แต่หากต้องการทำต่อ GraphQL query ก็สามารถทำอะไรบางอย่างกับ URL เหล่านี้ได้: ส่งทางอีเมล อัปโหลดไฟล์ผ่าน FTP ไปที่ใดที่หนึ่ง ติดตั้งในไซต์ InstaWP เป็นต้น)

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

เรสปอนส์มีดังนี้:

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

ทางเลือก: การรับข้อมูลรับรอง GitHub จาก HTTP request

เรายังสามารถอนุญาตให้ผู้ใช้ของเราระบุข้อมูลรับรอง GitHub ของตนเองผ่านเฮดเดอร์ได้

GraphQL query นี้เป็นการดัดแปลงจากอันก่อนหน้า โดยมีความแตกต่างดังนี้:

  • โอเปอเรชัน RetrieveGitHubAccessToken อ่านและเอ็กซ์พอร์ตค่าจากเฮดเดอร์ X-Github-Access-Token ของ HTTP request ปัจจุบัน และระบุว่าเฮดเดอร์นี้ไม่ได้ถูกส่งมาหรือไม่
  • FailIfGitHubAccessTokenIsMissing จะทำให้เกิดข้อผิดพลาดเมื่อไม่มีเฮดเดอร์
  • โอเปอเรชันอื่น ๆ ทั้งหมดได้เพิ่มไดเรกทีฟ @skip(if: $isGithubAccessTokenMissing) เพื่อไม่ให้ถูกรันเมื่อไม่มีโทเค็น
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
  # ...
}

เมื่อมีการส่งเฮดเดอร์ X-Github-Access-Token มา เรสปอนส์จะเหมือนกับด้านบน

เมื่อไม่ได้ส่งมา เรสปอนส์จะเป็นดังนี้:

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

เราสามารถดึงข้อมูลรับรองสำหรับหลายบริการที่ใช้ใน API gateway จากเฮดเดอร์ พร้อมทั้งตรวจสอบว่ามีการส่งมาครบทั้งหมด:

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

ทีละขั้นตอน: การสร้าง GraphQL query

ด้านล่างนี้คือการวิเคราะห์โดยละเอียดว่า query ทำงานอย่างไร

เอนด์พอยต์ที่จะเชื่อมต่อสามารถสร้างขึ้นแบบไดนามิกได้ ในกรณีนี้ใช้ _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
 
  # ...
}

เรสปอนส์จาก GitHub Actions API มีขนาดใหญ่และไม่ใช่สิ่งที่เราสนใจ ดังนั้นเราจึงใช้ @remove เพื่อนำออกจากเรสปอนส์ อย่างไรก็ตาม ในระหว่างการพัฒนา เราจะปิดใช้งานไดเรกทีฟนี้ เพื่อแสดงและทำความเข้าใจรูปแบบของ JSON object ที่ส่งกลับมา และระบุรายการข้อมูลที่เราต้องการแยกออกมา:

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
}

เรสปอนส์มีดังนี้:

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

รายการข้อมูลที่เราสนใจคือพร็อพเพอร์ตี "archive_download_url" เราจะนำทางไปยังรายการข้อมูลเหล่านี้แต่ละรายการภายในโครงสร้าง JSON object แยกค่านั้นออกมาด้วยฟิลด์ _objectProperty (ใช้งานผ่านไดเรกทีฟ @applyField) และแทนที่องค์ประกอบที่กำลังวนซ้ำด้วยการส่งอาร์กิวเมนต์ setResultInResponse: true:

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

เราเชื่อมต่อกับอาร์ติแฟกต์ URL ที่แยกออกมาทั้งหมดพร้อมกันผ่านฟิลด์ _sendHTTPRequests (ส่ง HTTP request หลายรายการแบบอะซิงโครนัส) และเราจะ query เฮดเดอร์ Location จากแต่ละเรสปอนส์

เนื่องจากฟิลด์ _sendHTTPRequests รับอาร์กิวเมนต์ input (ชนิด [HTTPRequestInput]) เราจึงสร้าง input นี้แบบไดนามิก โดย:

  • วนซ้ำอาร์ติแฟกต์ URL แต่ละรายการ (จัดเก็บไว้ภายใต้ตัวแปรไดนามิก $gitHubProxyArtifactDownloadURLs)
  • สร้าง JSON object สำหรับแต่ละรายการแบบไดนามิก (โดยใช้ฟิลด์ _objectAddEntry) ที่มีพารามิเตอร์ที่จำเป็นทั้งหมด (เฮดเดอร์ การยืนยันตัวตน และอื่น ๆ)
  • เพิ่ม URL เข้าไปใน JSON object นี้ (เข้าถึงได้ภายใต้ตัวแปรไดนามิก $url)

รายการของ JSON object ที่สร้างขึ้นแบบไดนามิกนี้จะถูกแปลงเป็น [HTTPRequestInput] เมื่อส่งเป็นอาร์กิวเมนต์ให้กับ _sendHTTPRequests(input:) หากขั้นตอนของเราไม่ถูกต้อง และมีรายการใดที่ไม่สามารถแปลงเป็น HTTPRequestInput ได้ (เช่น เพราะเราไม่ได้ระบุพร็อพเพอร์ตีที่จำเป็น หรือระบุพร็อพเพอร์ตีที่ไม่มีอยู่จริง) GraphQL server ก็จะทำให้เกิดข้อผิดพลาดในการแปลงค่า

สังเกตว่าเราต้องใช้ @remove กับฟิลด์ httpRequestInputs เนื่องจากมันมีโทเค็น GitHub อยู่ (ภายใต้ password: $githubAccessToken) ซึ่งเราไม่ต้องการพิมพ์ออกมาในเรสปอนส์ อย่างไรก็ตาม ในระหว่างการพัฒนา เราสามารถปิดใช้งานไดเรกทีฟนี้ได้

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

เนื่องจาก @remove ถูกคอมเมนต์ออกไปแล้ว ตอนนี้เราจึงสามารถแสดง JSON object input ที่สร้างขึ้นในเรสปอนส์ (ภายใต้เอนทรี httpRequestInputs) และเฮดเดอร์ Location ที่ได้จากแต่ละ HTTP เรสปอนส์ (ภายใต้เอเลียส 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"
      }
    ]
  }
}

สุดท้าย เราพิมพ์รายการ artifactDownloadURL ทั้งหมดรวมกันเป็นลิสต์ (เข้าถึงได้ภายใต้ตัวแปรไดนามิก $artifactDownloadURLs) โดยใช้ _echo:

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

ซึ่งจะพิมพ์ออกมาดังนี้:

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