diff --git a/apps/apps-cli/src/cli.ts b/apps/apps-cli/src/cli.ts index 1438464..92dee43 100644 --- a/apps/apps-cli/src/cli.ts +++ b/apps/apps-cli/src/cli.ts @@ -8,6 +8,7 @@ import { hideBin } from "yargs/helpers"; import { installAppCommand } from "./commands/install-app-command"; import { uninstallAppCommand } from "./commands/uninstall-app-command"; import "dotenv/config"; +import { webhooksCommand } from "./commands/webhooks-command"; const require = createRequire(import.meta.url); const pkg = require("../package.json"); @@ -117,6 +118,35 @@ const parser = yargs(hideBin(process.argv)) }); } ) + .command( + "webhooks", + "Print webhook details of installed app.", + (yargs) => { + return yargs + .option("instanceUrl", { + type: "string", + desc: "URL to the Saleor GraphQL API. Example: https://example.com/graphql/", + demandOption: true, + }) + .option("userEmail", { + type: "string", + desc: "Dashboard user email", + demandOption: true, + }) + .option("userPassword", { + type: "string", + desc: "Dashboard user password", + demandOption: true, + }); + }, + (argv) => { + webhooksCommand({ + instanceUrl: argv.instanceUrl, + userEmail: argv.userEmail, + userPassword: argv.userPassword, + }); + } + ) .demandCommand(1, "You need at least one command before moving on") .alias("h", "help") .wrap(null); diff --git a/apps/apps-cli/src/commands/uninstall-app-command.ts b/apps/apps-cli/src/commands/uninstall-app-command.ts index cb09ffc..4f56bc1 100644 --- a/apps/apps-cli/src/commands/uninstall-app-command.ts +++ b/apps/apps-cli/src/commands/uninstall-app-command.ts @@ -50,6 +50,11 @@ export const uninstallAppCommand = async ({ appListSpinner.succeed(); + if (!installedApps.length) { + console.log("No apps installed"); + return; + } + // Display CLI interface with multiselect if none of the filters were provided if (appId || appName || manifestUrl) { const filteredApps = filterApps({ diff --git a/apps/apps-cli/src/commands/webhooks-command.ts b/apps/apps-cli/src/commands/webhooks-command.ts new file mode 100644 index 0000000..58816d0 --- /dev/null +++ b/apps/apps-cli/src/commands/webhooks-command.ts @@ -0,0 +1,120 @@ +import ora from "ora"; +import { getAccessTokenMutation } from "../saleor-api/operations/get-access-token-mutation"; +import { getAppsListQuery } from "../saleor-api/operations/get-apps-list-query"; +import { select } from "@inquirer/prompts"; +import { getAppWebhooksQuery } from "../saleor-api/operations/get-app-webhooks-query"; +import { removeWebhookMutation } from "../saleor-api/operations/remove-webhook-mutation"; + +interface DumpMetadataCommandArgs { + instanceUrl: string; + userEmail: string; + userPassword: string; +} + +export const webhooksCommand = async ({ + instanceUrl, + userEmail, + userPassword, +}: DumpMetadataCommandArgs) => { + const loginSpinner = ora("Logging into Saleor instance").start(); + + const token = await getAccessTokenMutation({ + email: userEmail, + password: userPassword, + saleorApiUrl: instanceUrl, + }); + + loginSpinner.succeed(); + + const appListSpinner = ora("Fetching installed apps").start(); + + const installedApps = await getAppsListQuery({ + saleorApiUrl: instanceUrl, + token, + }); + + appListSpinner.succeed(); + + if (!installedApps.length) { + console.log("No apps installed"); + return; + } + + const appId = await select({ + message: "Select app", + choices: installedApps.map((app) => ({ + name: app.name ? `${app.name} (${app.id})` : app.id, + value: app.id, + })), + }); + + const webhooksData = await getAppWebhooksQuery({ + appId, + saleorApiUrl: instanceUrl, + token, + }); + + if (!webhooksData.length) { + console.log("Application has no webhooks configured"); + return; + } + + const webhook = await select({ + message: "Select webhook to investigate", + choices: webhooksData.map((webhook) => ({ + name: `${webhook.name} (${[...webhook.syncEvents, ...webhook.asyncEvents] + .map((e) => e.name) + .join(", ")})`, + value: webhook, + description: ` +Target url: ${webhook.targetUrl} +Active: ${webhook.isActive} +Captured event deliveries count: ${webhook.eventDeliveries?.edges.length} + `, + })), + }); + + const operation = await select({ + message: "Operation", + choices: [ + { + name: "List event deliveries", + value: "list", + }, + { + name: "Remove webhook", + value: "remove", + }, + ], + }); + + if (operation === "list") { + console.log("Number of entries: ", webhook.eventDeliveries?.edges.length); + for (const deliveryEdge of webhook.eventDeliveries?.edges ?? []) { + const delivery = deliveryEdge.node; + + console.log(` +Event type: ${delivery.eventType} +Created at: ${delivery.createdAt} +Status: ${delivery.status}`); + const attempts = delivery.attempts?.edges ?? []; + const lastAttempt = attempts[attempts.length - 1]?.node; + + if (lastAttempt) { + console.log(` +Date of the last attempt: ${lastAttempt.createdAt} +Status: ${lastAttempt.status}`); + } + } + } else if (operation === "remove") { + const removeSpinner = ora("Removing webhook...").start(); + + await removeWebhookMutation({ + saleorApiUrl: instanceUrl, + token, + webhookId: webhook.id, + }); + + removeSpinner.succeed(); + } +}; diff --git a/apps/apps-cli/src/saleor-api/operations/get-app-metadata-query.ts b/apps/apps-cli/src/saleor-api/operations/get-app-metadata-query.ts new file mode 100644 index 0000000..5d1ec6b --- /dev/null +++ b/apps/apps-cli/src/saleor-api/operations/get-app-metadata-query.ts @@ -0,0 +1,31 @@ +import request from "graphql-request"; + +import { graphql } from "../generated/gql"; + +const getAppMetadataQueryDocument = graphql(/* GraphQL */ ` + query GetAppMetadata { + app(id: "QXBwOjE=") { + metadata { + key + value + } + } + } +`); + +export const getAppMetadataQuery = async ({ + saleorApiUrl, + token, +}: { + saleorApiUrl: string; + token: string; +}) => { + const { app } = await request( + saleorApiUrl, + getAppMetadataQueryDocument, + {}, + { "Authorization-Bearer": token } + ); + + return app?.metadata ?? []; +}; diff --git a/apps/apps-cli/src/saleor-api/operations/get-app-webhooks-query.ts b/apps/apps-cli/src/saleor-api/operations/get-app-webhooks-query.ts new file mode 100644 index 0000000..c85b285 --- /dev/null +++ b/apps/apps-cli/src/saleor-api/operations/get-app-webhooks-query.ts @@ -0,0 +1,67 @@ +import request from "graphql-request"; + +import { graphql } from "../generated/gql"; + +const getAppWebhooksQueryDocument = graphql(/* GraphQL */ ` + query GetAppWebhooks($id: ID!) { + app(id: $id) { + webhooks { + id + name + isActive + syncEvents { + name + eventType + } + asyncEvents { + name + eventType + } + targetUrl + eventDeliveries(first: 10) { + edges { + node { + id + createdAt + status + eventType + attempts(first: 10) { + edges { + node { + id + createdAt + taskId + duration + response + status + } + } + } + } + } + } + } + } + } +`); + +export const getAppWebhooksQuery = async ({ + saleorApiUrl, + token, + appId, +}: { + saleorApiUrl: string; + token: string; + appId: string; +}) => { + const { app } = await request( + saleorApiUrl, + getAppWebhooksQueryDocument, + { + id: appId, + }, + { "Authorization-Bearer": token } + ); + + return app?.webhooks ?? []; +}; diff --git a/apps/apps-cli/src/saleor-api/operations/remove-webhook-mutation.ts b/apps/apps-cli/src/saleor-api/operations/remove-webhook-mutation.ts new file mode 100644 index 0000000..815ce30 --- /dev/null +++ b/apps/apps-cli/src/saleor-api/operations/remove-webhook-mutation.ts @@ -0,0 +1,40 @@ +import request from "graphql-request"; + +import { graphql } from "../generated/gql"; + +const removeWebhookMutationDocument = graphql(/* GraphQL */ ` + mutation RemoveWebhook($webhookId: ID!) { + webhookDelete(id: $webhookId) { + errors { + field + message + } + } + } +`); + +export const removeWebhookMutation = async ({ + saleorApiUrl, + token, + webhookId, +}: { + saleorApiUrl: string; + token: string; + webhookId: string; +}) => { + const { webhookDelete } = await request( + saleorApiUrl, + removeWebhookMutationDocument, + { + webhookId, + }, + { "Authorization-Bearer": token } + ); + + if (webhookDelete?.errors.length) { + console.log("Sth went wrong", webhookDelete.errors); + throw new Error(`Remove webhook mutation failed`); + } + + return; +};