Compare commits

...

6 commits

Author SHA1 Message Date
Krzysztof Wolski
a260414894 Add test for util function 2023-06-26 15:51:34 +02:00
Krzysztof Wolski
246789cb0c Update the lockfile 2023-06-26 12:58:11 +02:00
Krzysztof Wolski
4cb15a8033 Update the dependabot 2023-06-26 12:49:38 +02:00
Krzysztof Wolski
6b9f8ac32b Add changesets 2023-06-26 12:49:38 +02:00
Krzysztof Wolski
bd757776f2 Finish webhooks management POC 2023-06-26 12:49:38 +02:00
Krzysztof Wolski
7efa970079 Introduce app cli 2023-06-26 12:49:38 +02:00
27 changed files with 6658 additions and 2892 deletions

View file

@ -0,0 +1,6 @@
---
"@saleor/apps-cli": patch
---
Introduced Apps CLI.
The app is intended to help with common tasks in app development, like installing apps from manifest or debugging webhooks.

View file

@ -29,6 +29,11 @@ updates:
interval: "weekly" interval: "weekly"
# Apps # Apps
- package-ecosystem: "npm"
directory: "/apps/apps-cli"
open-pull-requests-limit: 0
schedule:
interval: "weekly"
- package-ecosystem: "npm" - package-ecosystem: "npm"
directory: "/apps/cms" directory: "/apps/cms"
open-pull-requests-limit: 0 open-pull-requests-limit: 0

View file

@ -0,0 +1,3 @@
INSTANCE_URL=https://demo.saleor.io/graphql/
USER_EMAIL=admin@example.com
USER_PASSWORD=password123

4
apps/apps-cli/.eslintrc Normal file
View file

@ -0,0 +1,4 @@
{
"root": true,
"extends": ["saleor"]
}

148
apps/apps-cli/.gitignore vendored Normal file
View file

@ -0,0 +1,148 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
package-lock.json
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# other
build/
.vscode/
binaries/
.saleor
token.txt
vendor/
test.js
dummy/
..bfg-report
.idea/
.DS_Store
package/
.type-coverage/
coverage-ts/

37
apps/apps-cli/README.md Normal file
View file

@ -0,0 +1,37 @@
### Install dependencies
This project uses [pnpm](https://pnpm.io) for managing dependencies
```
pnpm install
```
### Run Watch Mode
```
pnpm watch
```
### Run CLI
```
node build/cli.js ...
```
### Available commands
List of available commands:
```
node dist/app-cli.js -h
```
Description of available arguments:
```
node dist/app-cli.js [command name] -h
```
### Configuration
If options are not passed as arguments, cli will try to read environment variables. Example configuration is available in `.env.example` file.

18
apps/apps-cli/codegen.ts Normal file
View file

@ -0,0 +1,18 @@
/* eslint-disable import/no-default-export */
import { CodegenConfig } from "@graphql-codegen/cli";
const config: CodegenConfig = {
schema: "https://demo.saleor.io/graphql/",
documents: ["src/saleor-api/operations/*.ts"],
ignoreNoDocuments: true, // for better experience with the watcher
generates: {
"./src/saleor-api/generated/": {
preset: "client",
presetConfig: {
fragmentMasking: false,
},
},
},
};
export default config;

View file

@ -0,0 +1,67 @@
{
"name": "@saleor/apps-cli",
"description": "",
"version": "0.0.0",
"author": "Saleor",
"scripts": {
"build": "pnpm bundle",
"generate": "graphql-codegen",
"lint": "prettier --write . && eslint src/**/*.ts --cache --fix",
"test": "pnpm vitest",
"typecov": "type-coverage --cache",
"typecov-report": "typescript-coverage-report",
"watch": "concurrently \"npm:watch-*\"",
"watch-esbuild": "esbuild --watch src/cli.ts --bundle --minify --outfile=dist/apps-cli.js --platform=node --format=esm --target=node18 --banner:js=\"import { createRequire } from 'module';const require = createRequire(import.meta.url);import { dirname } from 'path'; import { fileURLToPath } from 'url'; const __dirname = dirname(fileURLToPath(import.meta.url));\" --out-extension:.js=.js",
"watch-generate": "graphql-codegen -w",
"watch-ts": "tsc --noEmit --watch --preserveWatchOutput"
},
"bin": {
"saleor": "./dist/apps-cli.js"
},
"dependencies": {
"@graphql-typed-document-node/core": "3.2.0",
"@inquirer/prompts": "^2.1.1",
"@oclif/core": "^1.26.2",
"@saleor/app-sdk": "0.40.1",
"chalk": "^5.2.0",
"date-fns": "^2.30.0",
"dotenv": "^16.3.1",
"enquirer": "^2.3.6",
"fs-extra": "^11.1.1",
"graphql": "^16.6.0",
"graphql-request": "^6.1.0",
"open": "^9.1.0",
"ora": "^6.3.1",
"semver": "^7.5.1",
"slugify": "^1.6.6",
"yargs": "^17.7.2"
},
"devDependencies": {
"@graphql-codegen/cli": "^4.0.1",
"@graphql-codegen/client-preset": "^4.0.1",
"@types/fs-extra": "^11.0.1",
"@types/node": "^20.3.1",
"@types/semver": "^7.5.0",
"@types/yargs": "^17.0.24",
"concurrently": "^8.2.0",
"esbuild": "^0.18.2",
"eslint": "^8.42.0",
"eslint-config-saleor": "workspace:*",
"pkg": "^5.8.1",
"prettier": "2.8.8",
"ts-node": "^10.9.1",
"tsm": "^2.3.0",
"type-coverage": "^2.26.0",
"typescript": "^5.1.3",
"typescript-coverage-report": "^0.7.0",
"vitest": "^0.32.0"
},
"engines": {
"node": "^18 || ^20"
},
"files": [
"dist/apps-cli.js"
],
"license": "BSD 3-Clause",
"type": "module"
}

158
apps/apps-cli/src/cli.ts Normal file
View file

@ -0,0 +1,158 @@
#!/usr/bin/env node
import chalk from "chalk";
import { createRequire } from "module";
import semver from "semver";
import yargs from "yargs";
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");
if (!semver.satisfies(process.versions.node, ">= 18")) {
console.error(`${chalk.red("ERROR")}: CLI requires Node.js 18.x or later`);
process.exit(1);
}
const parser = yargs(hideBin(process.argv))
.scriptName("apps-cli")
.version(pkg.version)
.alias("V", "version")
.usage("Usage: $0 <command> [options]")
.config({
instanceUrl: process.env.INSTANCE_URL,
userEmail: process.env.USER_EMAIL,
userPassword: process.env.USER_PASSWORD,
})
.command(
"installApp",
"Install an app on a Saleor instance based on provided manifest.",
(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,
})
.option("manifestUrl", {
type: "string",
desc: "URL to the app manifest. Example: https://example.com/api/manifest",
demandOption: true,
});
},
(argv) => {
installAppCommand({
instanceUrl: argv.instanceUrl,
userEmail: argv.userEmail,
userPassword: argv.userPassword,
manifestUrl: argv.manifestUrl,
});
}
)
.command(
"uninstallApp",
"If no filter is passed, CLI will display a list of installed apps and ask which one to remove. Otherwise all apps matching the filter will be removed.",
(yargs) => {
return yargs
.option("instanceUrl", {
type: "string",
desc: "URL to the Saleor GraphQL API",
demandOption: true,
})
.option("userEmail", {
type: "string",
desc: "Dashboard user email",
demandOption: true,
})
.option("userPassword", {
type: "string",
desc: "Dashboard user password",
demandOption: true,
})
.option("manifestUrl", {
type: "string",
desc: "Url to the app manifest which you want to remove",
})
.option("appName", {
type: "string",
desc: "Name of the app to remove",
})
.option("appId", {
type: "string",
desc: "If of the app to remove",
})
.option("all", {
type: "boolean",
default: false,
desc: "Will remove all apps",
})
.option("force", {
type: "boolean",
default: false,
desc: "No confirmation",
});
},
(argv) => {
uninstallAppCommand({
instanceUrl: argv.instanceUrl,
userEmail: argv.userEmail,
userPassword: argv.userPassword,
manifestUrl: argv.manifestUrl,
appId: argv.appId,
all: argv.all,
force: argv.force,
});
}
)
.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);
try {
await parser.parse();
} catch (error) {
console.log("parser error");
}

View file

@ -0,0 +1,35 @@
import { installAndWaitForResult } from "../lib/install-and-wait-for-result";
import { getAccessTokenMutation } from "../saleor-api/operations/get-access-token-mutation";
import ora from "ora";
interface InstallAppCommandArgs {
instanceUrl: string;
userEmail: string;
userPassword: string;
manifestUrl: string;
}
export const installAppCommand = async ({
instanceUrl,
manifestUrl,
userEmail,
userPassword,
}: InstallAppCommandArgs) => {
const loginSpinner = ora("Logging into Saleor instance").start();
const token = await getAccessTokenMutation({
email: userEmail,
password: userPassword,
saleorApiUrl: instanceUrl,
});
loginSpinner.succeed();
const installedAppData = await installAndWaitForResult({
saleorApiUrl: instanceUrl,
token,
appManifestUrl: manifestUrl,
});
console.log(`App ${installedAppData.name} (${installedAppData.id}) installed!`);
};

View file

@ -0,0 +1,110 @@
import { checkbox, confirm } from "@inquirer/prompts";
import { getAccessTokenMutation } from "../saleor-api/operations/get-access-token-mutation";
import { getAppsListQuery } from "../saleor-api/operations/get-apps-list-query";
import { uninstallAppMutation } from "../saleor-api/operations/uninstall-app-mutation";
import { filterApps } from "../lib/filter-apps";
import ora from "ora";
interface UninstallAppCommandArgs {
instanceUrl: string;
userEmail: string;
userPassword: string;
manifestUrl?: string;
appName?: string;
appId?: string;
all?: boolean;
force?: boolean;
}
export const uninstallAppCommand = async ({
instanceUrl,
manifestUrl,
userEmail,
userPassword,
all,
force,
appId,
appName,
}: UninstallAppCommandArgs) => {
const loginSpinner = ora("Logging into Saleor instance").start();
const token = await getAccessTokenMutation({
email: userEmail,
password: userPassword,
saleorApiUrl: instanceUrl,
});
loginSpinner.succeed();
const appIdsToRemove: string[] = [];
if (appId) {
appIdsToRemove.push(appId);
} else {
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;
}
// Display CLI interface with multiselect if none of the filters were provided
if (appId || appName || manifestUrl) {
const filteredApps = filterApps({
apps: installedApps,
filter: {
id: appId,
name: appName,
manifestUrl: manifestUrl,
},
});
appIdsToRemove.push(...filteredApps.map((app) => app.id));
} else if (all) {
appIdsToRemove.push(...installedApps.map((app) => app.id));
} else {
const selectedIds = await checkbox({
message: "Select apps to uninstall",
choices: installedApps.map((app) => ({
name: app.name ? `${app.name} (${app.id}) ${app.type}` : app.id,
value: app.id,
})),
});
appIdsToRemove.push(...selectedIds);
}
}
const confirmed = force
? true
: await confirm({
message: `${appIdsToRemove.length} apps will be removed. Continue?`,
default: false,
});
if (!confirmed) {
console.log("Operation aborted - no confirmation");
return;
}
const uninstallSpinner = ora("Uninstalling apps").start();
try {
await Promise.all(
appIdsToRemove.map((appId) =>
uninstallAppMutation({ saleorApiUrl: instanceUrl, token, id: appId })
)
);
} catch (e) {
uninstallSpinner.fail();
console.error(e);
return;
}
uninstallSpinner.succeed();
};

View file

@ -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();
}
};

View file

@ -0,0 +1,21 @@
import { AppManifest } from "@saleor/app-sdk/types";
export const fetchAppManifest = async (manifestUrl: string) => {
const manifestDataResponse = await fetch(manifestUrl);
let manifestData: AppManifest;
if (!manifestDataResponse.ok) {
console.log("Error fetching manifest");
throw new Error("Error fetching manifest");
}
try {
manifestData = (await manifestDataResponse.json()) as AppManifest;
} catch (e) {
console.log("Error parsing manifest");
throw new Error("Error parsing manifest");
}
return manifestData;
};

View file

@ -0,0 +1,65 @@
import { describe, expect, it } from "vitest";
import { AppDetailsFragment } from "../saleor-api/generated/graphql";
import { filterApps } from "./filter-apps";
const mockedApp1: AppDetailsFragment = {
id: "1",
name: "app1",
manifestUrl: "https://app1.com",
};
const mockedApp1Duplicate: AppDetailsFragment = {
id: "2",
name: "app1",
manifestUrl: "https://app1.com",
};
const mockedApp2: AppDetailsFragment = {
id: "3",
name: "app2",
manifestUrl: "https://app2.com",
};
const mockedAppList = [mockedApp1, mockedApp1Duplicate, mockedApp2];
describe("filterApps", function () {
it("Return the same apps, when no filters applied", async () => {
expect(
filterApps({
apps: mockedAppList,
filter: {},
})
).toStrictEqual(mockedAppList);
});
it("Return all apps with the same name, when filter name is applied", async () => {
expect(
filterApps({
apps: mockedAppList,
filter: {
name: mockedApp1.name!,
},
})
).toStrictEqual([mockedApp1, mockedApp1Duplicate]);
});
it("Return all apps with the same manifest, when filter manifest is applied", async () => {
expect(
filterApps({
apps: mockedAppList,
filter: {
manifestUrl: mockedApp1.manifestUrl!,
},
})
).toStrictEqual([mockedApp1, mockedApp1Duplicate]);
});
it("Return app with given id, when filter id is applied", async () => {
expect(
filterApps({
apps: mockedAppList,
filter: {
id: mockedApp1.id,
},
})
).toStrictEqual([mockedApp1]);
});
});

View file

@ -0,0 +1,28 @@
import { AppDetailsFragment } from "../saleor-api/generated/graphql";
interface FilterAppsArgs {
apps: AppDetailsFragment[];
filter: {
id?: string;
name?: string;
manifestUrl?: string;
};
}
export const filterApps = ({ apps, filter: { id, manifestUrl, name } }: FilterAppsArgs) => {
return apps.filter((app) => {
if (id && app.id !== id) {
return false;
}
if (name && app.name !== name) {
return false;
}
if (manifestUrl && app.manifestUrl !== manifestUrl) {
return false;
}
return true;
});
};

View file

@ -0,0 +1,94 @@
import ora from "ora";
import { fetchAppManifest } from "./fetch-app-manifest";
import { filterApps } from "./filter-apps";
import { getAppInstallationsListQuery } from "../saleor-api/operations/get-app-installations-list-query";
import { getAppsListQuery } from "../saleor-api/operations/get-apps-list-query";
import { installAppMutation } from "../saleor-api/operations/install-app-mutation";
interface InstallAndWaitForResultArgs {
saleorApiUrl: string;
token: string;
appManifestUrl: string;
}
function delay(timeMs: number) {
return new Promise((resolve) => setTimeout(resolve, timeMs));
}
/*
* Attempt to install app from the manifest, wait for the operation to complete
* and return app installation result.
* If will throw error if any of the steps fails.
*/
export const installAndWaitForResult = async ({
saleorApiUrl,
token,
appManifestUrl,
}: InstallAndWaitForResultArgs) => {
const manifestSpinner = ora("Fetching app manifest").start();
const manifestData = await fetchAppManifest(appManifestUrl);
manifestSpinner.succeed();
const installSpinner = ora("Installing the app").start();
const appInstallationJob = await installAppMutation({
manifestUrl: appManifestUrl,
saleorApiUrl: saleorApiUrl,
token,
appName: manifestData.name,
});
installSpinner.text = `Installing the app (job id: ${appInstallationJob.id})`;
// Lets give the API a bit of time to process installation
await delay(1000);
// App installation is on progress, now we have to monitor if it resolved. Wait max 20s for the result
for (let i = 0; i < 10; i++) {
const currentAppInstallations = await getAppInstallationsListQuery({
saleorApiUrl: saleorApiUrl,
token,
});
const appInstallation = currentAppInstallations.find((x) => x.id === appInstallationJob.id);
if (!appInstallation) {
// Job has been processed! If not on the list, it means it was successful
break;
}
if (appInstallation.status === "FAILED") {
installSpinner.fail("Installation failed");
throw new Error("App installation failed: " + appInstallation.message);
}
// Wait a bit and check again
await delay(2000);
}
installSpinner.text = "Confirming the app installed";
// App should be installed by now, fetch its details
const currentAppInstallations = await getAppsListQuery({
saleorApiUrl,
token,
});
const installedApp = filterApps({
apps: currentAppInstallations,
filter: {
manifestUrl: appManifestUrl,
},
});
if (!installedApp.length) {
// Investigate if this can happen - app not in the list of installed apps nor in the list of installations
throw new Error("App not found on the list of installed apps");
}
installSpinner.succeed("App installed!");
return installedApp[0];
};

View file

@ -0,0 +1,42 @@
import request from "graphql-request";
import { graphql } from "../generated/gql";
const getAccessTokenMutationDocument = graphql(/* GraphQL */ `
mutation GetAccessToken($email: String!, $password: String!) {
tokenCreate(email: $email, password: $password) {
token
refreshToken
errors {
field
message
}
}
}
`);
export const getAccessTokenMutation = async ({
saleorApiUrl,
email,
password,
}: {
saleorApiUrl: string;
password: string;
email: string;
}) => {
const { tokenCreate } = await request(saleorApiUrl, getAccessTokenMutationDocument, {
email,
password,
});
if (tokenCreate?.errors.length) {
console.log("mutation failed", tokenCreate?.errors);
throw new Error(`Get access token mutation failed - API returned errors`);
}
const token = tokenCreate?.token;
if (!token) {
throw new Error(`Get access token mutation failed - no token in the response`);
}
return token;
};

View file

@ -0,0 +1,30 @@
import request from "graphql-request";
import { graphql } from "../generated/gql";
const getAppInstallationsQueryDocument = graphql(/* GraphQL */ `
query GetAppInstallations {
appsInstallations {
id
status
message
}
}
`);
export const getAppInstallationsListQuery = async ({
saleorApiUrl,
token,
}: {
saleorApiUrl: string;
token: string;
}) => {
const { appsInstallations } = await request(
saleorApiUrl,
getAppInstallationsQueryDocument,
{},
{ "Authorization-Bearer": token }
);
return appsInstallations;
};

View file

@ -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 ?? [];
};

View file

@ -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 ?? [];
};

View file

@ -0,0 +1,48 @@
import request from "graphql-request";
import { graphql } from "../generated/gql";
export const AppDetailsFragment = graphql(/* GraphQL */ `
fragment AppDetails on App {
id
name
isActive
type
created
manifestUrl
}
`);
const getAppsQueryDocument = graphql(/* GraphQL */ `
query GetApps {
apps(
first: 100
filter: { type: THIRDPARTY }
sortBy: { field: CREATION_DATE, direction: DESC }
) {
totalCount
edges {
node {
...AppDetails
}
}
}
}
`);
export const getAppsListQuery = async ({
saleorApiUrl,
token,
}: {
saleorApiUrl: string;
token: string;
}) => {
const { apps } = await request(
saleorApiUrl,
getAppsQueryDocument,
{},
{ "Authorization-Bearer": token }
);
return apps?.edges.map(({ node }) => node) ?? [];
};

View file

@ -0,0 +1,58 @@
import request from "graphql-request";
import { graphql } from "../generated/gql";
const installAppMutationDocument = graphql(/* GraphQL */ `
mutation InstallApp($input: AppInstallInput!) {
appInstall(input: $input) {
appInstallation {
id
status
appName
}
errors {
field
message
}
}
}
`);
export const installAppMutation = async ({
saleorApiUrl,
token,
appName,
manifestUrl,
}: {
saleorApiUrl: string;
token: string;
manifestUrl: string;
appName: string;
}) => {
const { appInstall } = await request(
saleorApiUrl,
installAppMutationDocument,
{
input: {
manifestUrl,
activateAfterInstallation: true,
appName,
},
},
{ "Authorization-Bearer": token }
);
if (appInstall?.errors.length) {
console.log("Sth went wrong", appInstall.errors);
throw new Error(`Install app ${appName} mutation failed`);
}
if (!appInstall?.appInstallation) {
console.log("App installation not returned");
throw new Error(
`Install app ${appName} mutation failed - no app installation data in the response`
);
}
return appInstall?.appInstallation;
};

View file

@ -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;
};

View file

@ -0,0 +1,48 @@
import request from "graphql-request";
import { graphql } from "../generated/gql";
import { AppErrorCode } from "../generated/graphql";
const uninstallAppMutationDocument = graphql(/* GraphQL */ `
mutation UninstallApp($id: ID!) {
appDelete(id: $id) {
errors {
field
message
code
}
}
}
`);
export const uninstallAppMutation = async ({
saleorApiUrl,
token,
id,
}: {
saleorApiUrl: string;
token: string;
id: string;
}) => {
const { appDelete } = await request(
saleorApiUrl,
uninstallAppMutationDocument,
{
id,
},
{ "Authorization-Bearer": token }
);
if (appDelete?.errors.length) {
const error = appDelete.errors[0];
if (error.code === AppErrorCode.NotFound) {
throw new Error(`Uninstall app ${id} mutation failed - no installed app with this ID`);
}
throw new Error(
`Uninstall app ${id} mutation failed. API responded with error: ${error.code} - ${error.message}`
);
}
return;
};

View file

@ -0,0 +1,97 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */
/* Projects */
// "incremental": true, /* Enable incremental compilation */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "es2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
"lib": [
"es2021"
] /* Specify a set of bundled library declaration files that describe the target runtime environment. */,
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */
// "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
/* Modules */
"module": "ES2022" /* Specify what module code is generated. */,
// "rootDir": "./", /* Specify the root folder within your source files. */
"moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */,
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
"resolveJsonModule": true /* Enable importing .json files */,
// "noResolve": true, /* Disallow `import`s, `require`s or `<reference>`s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */
/* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */
"outDir": "./build" /* Specify an output folder for all emitted files. */,
// "removeComments": true, /* Disable emitting comments. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
/* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */,
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
/* Type Checking */
"strict": true /* Enable all strict type-checking options. */,
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */
// "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */
"useUnknownInCatchVariables": true /* Type catch clause variables as 'unknown' instead of 'any'. */,
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules"]
}

17
apps/apps-cli/turbo.json Normal file
View file

@ -0,0 +1,17 @@
{
"extends": [
"//"
],
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
"env": [
"APP_DEBUG",
"NODE_ENV",
"INSTANCE_URL",
"USER_EMAIL",
"USER_PASSWORD"
]
}
}
}

File diff suppressed because it is too large Load diff