บทเรียนที่ 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"
]
}
}