diff --git a/.changeset/api-catalog-commerce.md b/.changeset/api-catalog-commerce.md new file mode 100644 index 0000000..2fd0fe7 --- /dev/null +++ b/.changeset/api-catalog-commerce.md @@ -0,0 +1,7 @@ +--- +"@godaddy/cli": minor +--- + +Expand the built-in Commerce API catalog with additional domains and GraphQL metadata, and normalize Commerce scope tokens across generated endpoints. + +Also improves API command behavior by resolving templated catalog paths (for example, `/stores/{storeId}/...`), validating trusted absolute API hosts, and surfacing richer structured API error details for troubleshooting. \ No newline at end of file diff --git a/scripts/generate-api-catalog.ts b/scripts/generate-api-catalog.ts index 9c11c1c..23403c5 100644 --- a/scripts/generate-api-catalog.ts +++ b/scripts/generate-api-catalog.ts @@ -1,22 +1,43 @@ /** - * Build-time script: reads OpenAPI specs from specification submodules and - * produces a JSON catalog that the CLI bundles for `godaddy api list / describe`. + * Build-time script: discovers OpenAPI specs from gdcorp-platform repositories + * and produces a JSON catalog that the CLI bundles for: + * - godaddy api list + * - godaddy api describe * - * Resolves external $ref URLs (e.g. schemas.api.godaddy.com) at build time + * Also resolves external $ref URLs (e.g. schemas.api.godaddy.com) at build time * so the CLI catalog is fully self-contained. * * Usage: * pnpm tsx scripts/generate-api-catalog.ts * + * Optional environment variables: + * GITHUB_TOKEN GitHub token for higher API rate limits + * API_CATALOG_REPOS Comma-separated repo names to include + * (e.g. "commerce.catalog-products-specification,commerce.orders-specification") + * API_CATALOG_REPO_REFS Optional comma-separated repo=gitRef overrides + * (e.g. "commerce.catalog-products-specification=pull/81/head") + * API_CATALOG_INCLUDE_LEGACY_LOCATION + * "false" to exclude location.addresses-specification + * * Output: - * src/cli/schemas/api/manifest.json – domain index - * src/cli/schemas/api/.json – per-domain endpoint catalog + * src/cli/schemas/api/manifest.json – domain index + * src/cli/schemas/api/.json – per-domain endpoint catalog + * src/cli/schemas/api/registry.generated.ts – generated runtime registry */ +import { execFileSync } from "node:child_process"; import { lookup } from "node:dns/promises"; import * as fs from "node:fs"; import { isIP } from "node:net"; +import * as os from "node:os"; import * as path from "node:path"; +import { fileURLToPath } from "node:url"; +import { + Kind, + type TypeNode, + parse as parseGraphql, + print as printGraphql, +} from "graphql"; import { parse as parseYaml } from "yaml"; // --------------------------------------------------------------------------- @@ -31,6 +52,10 @@ interface OpenApiParameter { schema?: Record; } +interface OpenApiReference { + $ref: string; +} + interface OpenApiRequestBody { description?: string; required?: boolean; @@ -46,15 +71,19 @@ interface OpenApiOperation { operationId?: string; summary?: string; description?: string; - parameters?: OpenApiParameter[]; - requestBody?: OpenApiRequestBody; - responses?: Record; + parameters?: Array; + requestBody?: OpenApiRequestBody | OpenApiReference; + responses?: Record; security?: Array>; + "x-godaddy-graphql-schema"?: string; } interface OpenApiPathItem { - [method: string]: OpenApiOperation | OpenApiParameter[] | undefined; - parameters?: OpenApiParameter[]; + [method: string]: + | OpenApiOperation + | Array + | undefined; + parameters?: Array; } interface OpenApiServer { @@ -79,6 +108,30 @@ interface OpenApiSpec { // Output types — what the CLI consumes at runtime // --------------------------------------------------------------------------- +interface CatalogGraphqlArgument { + name: string; + type: string; + required: boolean; + description?: string; + defaultValue?: string; +} + +interface CatalogGraphqlOperation { + name: string; + kind: "query" | "mutation"; + returnType: string; + description?: string; + deprecated: boolean; + deprecationReason?: string; + args: CatalogGraphqlArgument[]; +} + +interface CatalogGraphqlSchema { + schemaRef: string; + operationCount: number; + operations: CatalogGraphqlOperation[]; +} + interface CatalogEndpoint { operationId: string; method: string; @@ -106,6 +159,7 @@ interface CatalogEndpoint { } >; scopes: string[]; + graphql?: CatalogGraphqlSchema; } interface CatalogDomain { @@ -121,33 +175,64 @@ interface CatalogManifest { generated: string; domains: Record< string, - { file: string; title: string; endpointCount: number } + { + file: string; + title: string; + endpointCount: number; + } >; } -// --------------------------------------------------------------------------- -// Spec source registry — add new spec submodules here -// --------------------------------------------------------------------------- +interface GitHubRepo { + name: string; + cloneUrl: string; + archived: boolean; + disabled: boolean; + private: boolean; +} interface SpecSource { - /** Domain key used in the CLI (e.g. "location-addresses") */ domain: string; - /** Relative path from workspace root to the OpenAPI YAML file */ - specPath: string; + repoName: string; + specFile: string; + specVersion: string; +} + +interface DiscoveredSpecSources { + sources: SpecSource[]; + cloneRoot: string | null; } -const __dirname = path.dirname(new URL(import.meta.url).pathname); -const WORKSPACE_ROOT = path.resolve(__dirname, "../.."); -const OUTPUT_DIR = path.resolve(__dirname, "../src/cli/schemas/api"); +// --------------------------------------------------------------------------- +// Discovery configuration +// --------------------------------------------------------------------------- -const SPEC_SOURCES: SpecSource[] = [ - { - domain: "location-addresses", - specPath: "location.addresses-specification/v1/schemas/openapi.yaml", - }, - // Add more spec submodules here as they become available: - // { domain: "catalog", specPath: "catalog-specification/v1/schemas/openapi.yaml" }, +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const PROJECT_ROOT = path.resolve(__dirname, ".."); +const OUTPUT_DIR = path.resolve(PROJECT_ROOT, "src/cli/schemas/api"); +const REGISTRY_FILE = path.join(OUTPUT_DIR, "registry.generated.ts"); + +const GITHUB_ORG = "gdcorp-platform"; +const GITHUB_API_BASE = "https://api.github.com"; +const GITHUB_REPOS_PAGE_SIZE = 100; +const COMMERCE_SPEC_REPO_PATTERN = /^commerce\.[a-z0-9-]+-specification$/; +const BOOTSTRAP_COMMERCE_REPOS = [ + "commerce.catalog-products-specification", + "commerce.orders-specification", + "commerce.stores-specification", + "commerce.fulfillments-specification", + "commerce.metafields-specification", + "commerce.transactions-specification", + "commerce.businesses-specification", + "commerce.bulk-operations-specification", + "commerce.channels-specification", + "commerce.onboarding-specification", ]; +const LEGACY_ALWAYS_INCLUDE_REPOS = ["location.addresses-specification"]; + +// --------------------------------------------------------------------------- +// External $ref resolution security controls +// --------------------------------------------------------------------------- const ALLOWED_REF_HOSTS = new Set(["schemas.api.godaddy.com"]); const MAX_REF_REDIRECTS = 5; @@ -409,24 +494,18 @@ async function fetchExternalRef(url: string): Promise> { /** * Resolve a potentially relative $ref URL against a base URL. - * "./country-code.yaml" resolved against - * "https://schemas.api.godaddy.com/common-types/v1/schemas/yaml/address.yaml" - * becomes "https://schemas.api.godaddy.com/common-types/v1/schemas/yaml/country-code.yaml" */ function resolveRefUrl(ref: string, baseUrl?: string): string | null { if (ref.startsWith("https://") || ref.startsWith("http://")) return ref; if (ref.startsWith("#")) return null; // local JSON pointer — skip if (!baseUrl) return null; - // Relative path: resolve against the base URL's directory const base = baseUrl.substring(0, baseUrl.lastIndexOf("/") + 1); return new URL(ref, base).toString(); } /** * Walk an object tree and resolve any { $ref: "..." } nodes by - * fetching the URL and inlining the result. Resolves both absolute - * and relative $refs (relative to parentUrl). Local JSON pointer - * refs (e.g. "#/components/schemas/Foo") are left as-is. + * fetching the URL and inlining the result. */ async function resolveRefs(obj: unknown, parentUrl?: string): Promise { if (obj === null || obj === undefined) return obj; @@ -438,20 +517,16 @@ async function resolveRefs(obj: unknown, parentUrl?: string): Promise { const record = obj as Record; - // Check if this node is a $ref if (typeof record.$ref === "string") { const resolvedUrl = resolveRefUrl(record.$ref, parentUrl); if (resolvedUrl) { const resolved = await fetchExternalRef(resolvedUrl); - // Preserve sibling properties (e.g. "description" next to "$ref") const { $ref, ...siblings } = record; const merged = { ...resolved, ...siblings }; - // Recursively resolve any nested $refs, using this URL as the new base return resolveRefs(merged, resolvedUrl); } } - // Recurse into all properties const result: Record = {}; for (const [key, value] of Object.entries(record)) { result[key] = await resolveRefs(value, parentUrl); @@ -460,7 +535,381 @@ async function resolveRefs(obj: unknown, parentUrl?: string): Promise { } // --------------------------------------------------------------------------- -// Helpers +// GitHub discovery helpers +// --------------------------------------------------------------------------- + +function parseRepoOverride(): string[] | null { + const raw = process.env.API_CATALOG_REPOS?.trim(); + if (!raw) return null; + + const repos = raw + .split(",") + .map((entry) => entry.trim()) + .filter((entry) => entry.length > 0); + + return repos.length > 0 ? repos : null; +} + +function parseRepoRefsOverride(): Map { + const raw = process.env.API_CATALOG_REPO_REFS?.trim(); + if (!raw) return new Map(); + + const refs = new Map(); + + for (const entry of raw.split(",")) { + const trimmed = entry.trim(); + if (!trimmed) continue; + + const separatorIndex = trimmed.indexOf("="); + if (separatorIndex <= 0 || separatorIndex === trimmed.length - 1) { + console.error( + `WARNING: invalid API_CATALOG_REPO_REFS entry '${trimmed}' (expected repo=ref)`, + ); + continue; + } + + const repoName = trimmed.slice(0, separatorIndex).trim(); + const ref = trimmed.slice(separatorIndex + 1).trim(); + if (!repoName || !ref) continue; + + refs.set(repoName, ref); + } + + return refs; +} + +function includeLegacyLocationRepo(): boolean { + const raw = process.env.API_CATALOG_INCLUDE_LEGACY_LOCATION; + if (!raw) return true; + return raw.toLowerCase() !== "false"; +} + +function githubHeaders(): Record { + const headers: Record = { + Accept: "application/vnd.github+json", + "User-Agent": "godaddy-cli-api-catalog-generator", + }; + + const token = process.env.GITHUB_TOKEN?.trim(); + if (token) { + headers.Authorization = `Bearer ${token}`; + } + + return headers; +} + +function isGitHubRepoObject(value: unknown): value is { + name: string; + clone_url: string; + archived: boolean; + disabled: boolean; + private: boolean; +} { + if (typeof value !== "object" || value === null) return false; + const record = value as Record; + return ( + typeof record.name === "string" && + typeof record.clone_url === "string" && + typeof record.archived === "boolean" && + typeof record.disabled === "boolean" && + typeof record.private === "boolean" + ); +} + +async function listReposForOwnerPath(ownerPath: string): Promise { + const repos: GitHubRepo[] = []; + + for (let page = 1; ; page += 1) { + const url = `${GITHUB_API_BASE}/${ownerPath}/${GITHUB_ORG}/repos?per_page=${GITHUB_REPOS_PAGE_SIZE}&page=${page}&type=public&sort=full_name&direction=asc`; + + const response = await fetch(url, { headers: githubHeaders() }); + if (!response.ok) { + if (response.status === 404) { + return []; + } + throw new Error( + `GitHub API request failed (${response.status}) while listing ${ownerPath} repos`, + ); + } + + const payload = (await response.json()) as unknown; + if (!Array.isArray(payload)) { + throw new Error("Unexpected GitHub API response: expected an array"); + } + + if (payload.length === 0) break; + + for (const item of payload) { + if (!isGitHubRepoObject(item)) { + continue; + } + repos.push({ + name: item.name, + cloneUrl: item.clone_url, + archived: item.archived, + disabled: item.disabled, + private: item.private, + }); + } + + if (payload.length < GITHUB_REPOS_PAGE_SIZE) { + break; + } + } + + return repos; +} + +async function listOrgRepos(): Promise { + const orgRepos = await listReposForOwnerPath("orgs"); + if (orgRepos.length > 0) { + return orgRepos; + } + + const userRepos = await listReposForOwnerPath("users"); + if (userRepos.length > 0) { + return userRepos; + } + + return []; +} + +function deriveDomainFromRepoName(repoName: string): string { + const withoutSuffix = repoName.endsWith("-specification") + ? repoName.slice(0, -"-specification".length) + : repoName; + + const withoutCommercePrefix = withoutSuffix.startsWith("commerce.") + ? withoutSuffix.slice("commerce.".length) + : withoutSuffix; + + return withoutCommercePrefix + .toLowerCase() + .replace(/\./g, "-") + .replace(/[^a-z0-9-]/g, "-") + .replace(/-+/g, "-") + .replace(/^-|-$/g, ""); +} + +function parseVersionDirectory(versionDirName: string): number[] | null { + if (!/^v\d+(?:\.\d+)*$/.test(versionDirName)) { + return null; + } + + const numeric = versionDirName.slice(1); + const parts = numeric.split(".").map((part) => Number.parseInt(part, 10)); + + if (parts.some((part) => Number.isNaN(part))) { + return null; + } + + return parts; +} + +function compareVersionArrays(a: number[], b: number[]): number { + const length = Math.max(a.length, b.length); + for (let index = 0; index < length; index += 1) { + const aPart = a[index] ?? 0; + const bPart = b[index] ?? 0; + if (aPart !== bPart) return aPart - bPart; + } + return 0; +} + +function findLatestSpecFile( + repoDir: string, +): { version: string; specFile: string } | null { + const versionCandidates = fs + .readdirSync(repoDir, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .map((entry) => entry.name) + .map((name) => ({ name, parsed: parseVersionDirectory(name) })) + .filter( + (entry): entry is { name: string; parsed: number[] } => + entry.parsed !== null, + ) + .sort((left, right) => compareVersionArrays(left.parsed, right.parsed)); + + for (let index = versionCandidates.length - 1; index >= 0; index -= 1) { + const version = versionCandidates[index].name; + + const candidates = [ + path.join(repoDir, version, "schemas", "openapi.yaml"), + path.join(repoDir, version, "schemas", "openapi.yml"), + path.join(repoDir, version, "schemas", "openapi.json"), + ]; + + for (const candidate of candidates) { + if (fs.existsSync(candidate)) { + return { version, specFile: candidate }; + } + } + } + + return null; +} + +function checkoutRepositoryRef(targetDir: string, ref: string): void { + execFileSync( + "git", + ["-C", targetDir, "fetch", "--depth", "1", "origin", ref], + { + stdio: "pipe", + env: process.env, + }, + ); + + execFileSync("git", ["-C", targetDir, "checkout", "--quiet", "FETCH_HEAD"], { + stdio: "pipe", + env: process.env, + }); +} + +function cloneRepository( + cloneUrl: string, + targetDir: string, + ref?: string, +): void { + execFileSync( + "git", + ["clone", "--depth", "1", "--quiet", cloneUrl, targetDir], + { + stdio: "pipe", + env: process.env, + }, + ); + + if (ref) { + checkoutRepositoryRef(targetDir, ref); + } +} + +async function discoverSpecSources(): Promise { + const allRepos = await listOrgRepos(); + const repoMap = new Map(allRepos.map((repo) => [repo.name, repo])); + + const overrides = parseRepoOverride(); + const repoRefOverrides = parseRepoRefsOverride(); + + const selectedRepoNames = new Set(); + if (overrides) { + for (const repoName of overrides) { + selectedRepoNames.add(repoName); + } + } else { + for (const repo of allRepos) { + if (COMMERCE_SPEC_REPO_PATTERN.test(repo.name)) { + selectedRepoNames.add(repo.name); + } + } + } + + if (!overrides && selectedRepoNames.size === 0) { + console.error( + "WARNING: Dynamic GitHub discovery found no commerce specifications. Falling back to bootstrap repository list.", + ); + for (const repoName of BOOTSTRAP_COMMERCE_REPOS) { + selectedRepoNames.add(repoName); + } + } + + if (includeLegacyLocationRepo()) { + for (const legacyRepo of LEGACY_ALWAYS_INCLUDE_REPOS) { + selectedRepoNames.add(legacyRepo); + } + } + + const selectedRepos = [...selectedRepoNames] + .map((name) => { + const discovered = repoMap.get(name); + if (discovered) { + return discovered; + } + + return { + name, + cloneUrl: `https://github.com/${GITHUB_ORG}/${name}.git`, + archived: false, + disabled: false, + private: false, + } satisfies GitHubRepo; + }) + .filter((repo) => !repo.private) + .filter((repo) => !repo.archived) + .filter((repo) => !repo.disabled) + .sort((a, b) => a.name.localeCompare(b.name)); + + if (!overrides && allRepos.length === 0) { + console.error( + "WARNING: GitHub repo discovery returned 0 repositories. Set API_CATALOG_REPOS or provide GITHUB_TOKEN for broader discovery.", + ); + } + + if (selectedRepos.length === 0) { + return { sources: [], cloneRoot: null }; + } + + const cloneRoot = fs.mkdtempSync( + path.join(os.tmpdir(), "godaddy-api-catalog-"), + ); + const sources: SpecSource[] = []; + const usedDomains = new Set(); + + for (const repo of selectedRepos) { + const repoDir = path.join(cloneRoot, repo.name); + const repoRef = repoRefOverrides.get(repo.name); + + try { + cloneRepository(repo.cloneUrl, repoDir, repoRef); + if (repoRef) { + console.log(` ${repo.name}: checked out override ref '${repoRef}'`); + } + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + const context = repoRef ? ` @ ref '${repoRef}'` : ""; + console.error( + `WARNING: failed to clone ${repo.name}${context}: ${message}`, + ); + continue; + } + + const latestSpec = findLatestSpecFile(repoDir); + if (!latestSpec) { + console.error( + `WARNING: ${repo.name} has no v*/schemas/openapi.{yaml,yml,json} — skipping`, + ); + continue; + } + + const domain = deriveDomainFromRepoName(repo.name); + if (!domain) { + console.error( + `WARNING: could not derive domain key from repo '${repo.name}' — skipping`, + ); + continue; + } + + if (usedDomains.has(domain)) { + console.error( + `WARNING: duplicate derived domain '${domain}' from repo '${repo.name}' — skipping`, + ); + continue; + } + + usedDomains.add(domain); + sources.push({ + domain, + repoName: repo.name, + specFile: latestSpec.specFile, + specVersion: latestSpec.version, + }); + } + + return { sources, cloneRoot }; +} + +// --------------------------------------------------------------------------- +// Spec processing helpers // --------------------------------------------------------------------------- const HTTP_METHODS = new Set([ @@ -486,40 +935,260 @@ function resolveBaseUrl(servers?: OpenApiServer[]): string { return url; } +const COMMERCE_SCOPE_URN_REGEX = + /^urn:godaddy:services:commerce\.([a-z0-9-]+):([a-z-]+)$/i; +const COMMERCE_SCOPE_URI_REGEX = + /^https:\/\/uri\.godaddy\.com\/services\/commerce\/([a-z0-9-]+)\/([a-z-]+)$/i; + +function normalizeScopeAction(action: string): string { + const normalized = action.toLowerCase(); + if (normalized === "read-write") { + return "write"; + } + return normalized; +} + +function normalizeScopeToken(scope: string): string { + const trimmed = scope.trim(); + if (!trimmed) return trimmed; + + const urnMatch = trimmed.match(COMMERCE_SCOPE_URN_REGEX); + if (urnMatch) { + const domain = urnMatch[1].toLowerCase(); + const action = normalizeScopeAction(urnMatch[2]); + return `commerce.${domain}:${action}`; + } + + const uriMatch = trimmed.match(COMMERCE_SCOPE_URI_REGEX); + if (uriMatch) { + const domain = uriMatch[1].toLowerCase(); + const action = normalizeScopeAction(uriMatch[2]); + return `commerce.${domain}:${action}`; + } + + const commerceMatch = trimmed.match(/^commerce\.([a-z0-9-]+):([a-z-]+)$/i); + if (commerceMatch) { + const domain = commerceMatch[1].toLowerCase(); + const action = normalizeScopeAction(commerceMatch[2]); + return `commerce.${domain}:${action}`; + } + + return trimmed; +} + function extractScopes(security?: Array>): string[] { if (!security) return []; - const scopes: string[] = []; + + const normalizedScopes = new Set(); + for (const entry of security) { for (const scopeList of Object.values(entry)) { - scopes.push(...scopeList); + for (const rawScope of scopeList) { + const normalizedScope = normalizeScopeToken(rawScope); + if (normalizedScope) { + normalizedScopes.add(normalizedScope); + } + } } } - return [...new Set(scopes)]; + + return [...normalizedScopes]; +} + +const graphqlSchemaCache = new Map(); + +function graphqlTypeToString(typeNode: TypeNode): string { + switch (typeNode.kind) { + case Kind.NAMED_TYPE: + return typeNode.name.value; + case Kind.NON_NULL_TYPE: + return `${graphqlTypeToString(typeNode.type)}!`; + case Kind.LIST_TYPE: + return `[${graphqlTypeToString(typeNode.type)}]`; + } +} + +function parseGraphqlOperations( + schemaSource: string, +): CatalogGraphqlOperation[] { + const document = parseGraphql(schemaSource, { noLocation: true }); + const operations: CatalogGraphqlOperation[] = []; + + for (const definition of document.definitions) { + if (definition.kind !== Kind.OBJECT_TYPE_DEFINITION) continue; + + const typeName = definition.name.value; + if (typeName !== "Query" && typeName !== "Mutation") continue; + + const kind: CatalogGraphqlOperation["kind"] = + typeName === "Query" ? "query" : "mutation"; + + for (const field of definition.fields ?? []) { + const deprecatedDirective = field.directives?.find( + (directive) => directive.name.value === "deprecated", + ); + const deprecationReasonArg = deprecatedDirective?.arguments?.find( + (arg) => arg.name.value === "reason", + ); + + const deprecationReason = deprecationReasonArg + ? deprecationReasonArg.value.kind === Kind.STRING + ? deprecationReasonArg.value.value + : printGraphql(deprecationReasonArg.value) + : undefined; + + const args: CatalogGraphqlArgument[] = (field.arguments ?? []).map( + (arg) => ({ + name: arg.name.value, + type: graphqlTypeToString(arg.type), + required: + arg.type.kind === Kind.NON_NULL_TYPE && + arg.defaultValue === undefined, + description: arg.description?.value, + defaultValue: + arg.defaultValue === undefined + ? undefined + : printGraphql(arg.defaultValue), + }), + ); + + operations.push({ + name: field.name.value, + kind, + returnType: graphqlTypeToString(field.type), + description: field.description?.value, + deprecated: deprecatedDirective !== undefined, + deprecationReason, + args, + }); + } + } + + return operations.sort((left, right) => { + if (left.kind === right.kind) { + return left.name.localeCompare(right.name); + } + return left.kind === "query" ? -1 : 1; + }); +} + +function loadGraphqlSchemaMetadata( + specFile: string, + schemaRef: string, +): CatalogGraphqlSchema { + const specDir = path.dirname(specFile); + const resolvedSchemaPath = path.resolve(specDir, schemaRef); + const cacheKey = `${specFile}::${resolvedSchemaPath}`; + + const cached = graphqlSchemaCache.get(cacheKey); + if (cached) return cached; + + if (!fs.existsSync(resolvedSchemaPath)) { + throw new Error( + `GraphQL schema file not found for '${schemaRef}' (resolved: ${resolvedSchemaPath})`, + ); + } + + const schemaSource = fs.readFileSync(resolvedSchemaPath, "utf-8"); + const operations = parseGraphqlOperations(schemaSource); + + const metadata: CatalogGraphqlSchema = { + schemaRef, + operationCount: operations.length, + operations, + }; + + graphqlSchemaCache.set(cacheKey, metadata); + return metadata; +} + +function resolveLocalRef( + spec: OpenApiSpec, + ref: string, +): Record | null { + if (!ref.startsWith("#/")) return null; + + const segments = ref + .slice(2) + .split("/") + .map((segment) => segment.replace(/~1/g, "/").replace(/~0/g, "~")); + + let current: unknown = spec as unknown; + for (const segment of segments) { + if (typeof current !== "object" || current === null) { + return null; + } + + const record = current as Record; + current = record[segment]; + } + + if (typeof current !== "object" || current === null) { + return null; + } + + return current as Record; +} + +function resolveParameter( + spec: OpenApiSpec, + parameter: OpenApiParameter | OpenApiReference, +): OpenApiParameter | null { + if ("$ref" in parameter) { + const resolved = resolveLocalRef(spec, parameter.$ref); + if (!resolved) return null; + + const name = resolved.name; + const location = resolved.in; + if (typeof name !== "string" || typeof location !== "string") { + return null; + } + + return { + name, + in: location, + required: + typeof resolved.required === "boolean" ? resolved.required : undefined, + description: + typeof resolved.description === "string" + ? resolved.description + : undefined, + schema: + typeof resolved.schema === "object" && resolved.schema !== null + ? (resolved.schema as Record) + : undefined, + }; + } + + return parameter; } function processOperation( + spec: OpenApiSpec, + specFile: string, httpMethod: string, pathStr: string, operation: OpenApiOperation, - pathLevelParams?: OpenApiParameter[], + pathLevelParams?: Array, ): CatalogEndpoint { - // Merge path-level and operation-level parameters const allParams = [ ...(pathLevelParams || []), ...(operation.parameters || []), ]; - const parameters = allParams.map((p) => ({ - name: p.name, - in: p.in, - required: p.required ?? false, - description: p.description, - schema: p.schema, - })); + const parameters = allParams + .map((parameter) => resolveParameter(spec, parameter)) + .filter((parameter): parameter is OpenApiParameter => parameter !== null) + .map((parameter) => ({ + name: parameter.name, + in: parameter.in, + required: parameter.required ?? false, + description: parameter.description, + schema: parameter.schema, + })); - // Process request body let requestBody: CatalogEndpoint["requestBody"]; - if (operation.requestBody) { + if (operation.requestBody && !("$ref" in operation.requestBody)) { const rb = operation.requestBody; const contentTypes = rb.content ? Object.keys(rb.content) : []; const primaryCt = contentTypes[0] || "application/json"; @@ -529,11 +1198,10 @@ function processOperation( required: rb.required ?? false, description: rb.description, contentType: primaryCt, - schema: schema, + schema, }; } - // Process responses (skip $ref responses that we can't resolve inline) const responses: CatalogEndpoint["responses"] = {}; if (operation.responses) { for (const [status, resp] of Object.entries(operation.responses)) { @@ -552,11 +1220,16 @@ function processOperation( } } - // Generate a stable operationId if missing const operationId = operation.operationId || `${httpMethod}${pathStr.replace(/[^a-zA-Z0-9]/g, "_")}`; + let graphql: CatalogGraphqlSchema | undefined; + const graphqlSchemaRef = operation["x-godaddy-graphql-schema"]; + if (typeof graphqlSchemaRef === "string" && graphqlSchemaRef.length > 0) { + graphql = loadGraphqlSchemaMetadata(specFile, graphqlSchemaRef); + } + return { operationId, method: httpMethod.toUpperCase(), @@ -567,23 +1240,33 @@ function processOperation( requestBody, responses, scopes: extractScopes(operation.security), + graphql, }; } -function processSpec(spec: OpenApiSpec, domain: string): CatalogDomain { +function processSpec( + spec: OpenApiSpec, + domain: string, + specFile: string, +): CatalogDomain { const baseUrl = resolveBaseUrl(spec.servers); const endpoints: CatalogEndpoint[] = []; for (const [pathStr, pathItem] of Object.entries(spec.paths)) { - const pathLevelParams = pathItem.parameters as - | OpenApiParameter[] - | undefined; + const pathLevelParams = pathItem.parameters; for (const [key, value] of Object.entries(pathItem)) { if (key === "parameters" || !HTTP_METHODS.has(key) || !value) continue; const operation = value as OpenApiOperation; endpoints.push( - processOperation(key, pathStr, operation, pathLevelParams), + processOperation( + spec, + specFile, + key, + pathStr, + operation, + pathLevelParams, + ), ); } } @@ -598,6 +1281,107 @@ function processSpec(spec: OpenApiSpec, domain: string): CatalogDomain { }; } +function repairMissingJsonCommas(raw: string): string { + const lines = raw.split("\n"); + + for (let index = 0; index < lines.length - 1; index += 1) { + const currentLine = lines[index]; + const nextLine = lines[index + 1]; + + const currentTrimmedRight = currentLine.trimEnd(); + const nextTrimmedLeft = nextLine.trimStart(); + + if (currentTrimmedRight.length === 0) continue; + if (/[,\[{]\s*$/.test(currentTrimmedRight)) continue; + if (!/^"[^"\\]+"\s*:/.test(nextTrimmedLeft)) continue; + if (!/^\s*"[^"\\]+"\s*:/.test(currentTrimmedRight)) continue; + if (!/["}\]0-9a-zA-Z]$/.test(currentTrimmedRight)) continue; + + lines[index] = `${currentLine},`; + } + + return lines.join("\n"); +} + +function parseOpenApiSpec(raw: string, specFile: string): OpenApiSpec { + try { + return parseYaml(raw) as OpenApiSpec; + } catch (error) { + const lowerPath = specFile.toLowerCase(); + if (!lowerPath.endsWith(".json")) { + throw error; + } + + const repaired = repairMissingJsonCommas(raw); + if (repaired === raw) { + throw error; + } + + try { + console.error( + `WARNING: detected malformed JSON in ${specFile}; attempting comma-repair fallback`, + ); + return parseYaml(repaired) as OpenApiSpec; + } catch { + throw error; + } + } +} + +function identifierForDomain(domain: string): string { + const clean = domain.replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_+|_+$/g, ""); + + if (!clean) return "domain_json"; + if (/^[0-9]/.test(clean)) return `domain_${clean}`; + return `${clean}_json`; +} + +function writeRegistryFile(domains: string[]): void { + const sorted = [...domains].sort((a, b) => a.localeCompare(b)); + + const importLines = sorted.map((domain) => { + const identifier = identifierForDomain(domain); + return `import ${identifier} from "./${domain}.json";`; + }); + + const objectLines = sorted.map((domain) => { + const identifier = identifierForDomain(domain); + return ` "${domain}": ${identifier},`; + }); + + const content = [ + "/**", + " * AUTO-GENERATED by scripts/generate-api-catalog.ts", + " * Do not edit manually.", + " */", + "", + ...importLines, + "", + "export const DOMAIN_REGISTRY: Record = {", + ...objectLines, + "};", + "", + ].join("\n"); + + fs.writeFileSync(REGISTRY_FILE, content, "utf-8"); +} + +function removeStaleDomainJsonFiles(activeDomainFiles: Set): void { + const entries = fs.readdirSync(OUTPUT_DIR, { withFileTypes: true }); + for (const entry of entries) { + if (!entry.isFile()) continue; + if (!entry.name.endsWith(".json")) continue; + if (entry.name === "manifest.json") continue; + + if (!activeDomainFiles.has(entry.name)) { + fs.rmSync(path.join(OUTPUT_DIR, entry.name), { + recursive: false, + force: true, + }); + } + } +} + // --------------------------------------------------------------------------- // Main // --------------------------------------------------------------------------- @@ -605,61 +1389,88 @@ function processSpec(spec: OpenApiSpec, domain: string): CatalogDomain { async function main() { fs.mkdirSync(OUTPUT_DIR, { recursive: true }); + const { sources: specSources, cloneRoot } = await discoverSpecSources(); + const manifest: CatalogManifest = { generated: new Date().toISOString(), domains: {}, }; + const activeDomainFiles = new Set(); let totalEndpoints = 0; - for (const source of SPEC_SOURCES) { - const specFile = path.join(WORKSPACE_ROOT, source.specPath); - - if (!fs.existsSync(specFile)) { - console.error(`WARNING: spec not found: ${specFile} — skipping`); - continue; + try { + if (specSources.length === 0) { + throw new Error( + "No specification repositories discovered. Refusing to overwrite catalog output.", + ); } - const raw = fs.readFileSync(specFile, "utf-8"); - const spec = parseYaml(raw) as OpenApiSpec; - const catalog = processSpec(spec, source.domain); + for (const source of specSources) { + if (!fs.existsSync(source.specFile)) { + throw new Error( + `spec file not found for ${source.repoName}: ${source.specFile}`, + ); + } - // Resolve all external $refs in the catalog - console.log(` Resolving external $refs for ${source.domain}...`); - const resolved = (await resolveRefs(catalog)) as CatalogDomain; + try { + const raw = fs.readFileSync(source.specFile, "utf-8"); + const spec = parseOpenApiSpec(raw, source.specFile); + const catalog = processSpec(spec, source.domain, source.specFile); - const filename = `${source.domain}.json`; + console.log( + ` Resolving external $refs for ${source.domain} (${source.repoName}/${source.specVersion})...`, + ); + const resolved = (await resolveRefs(catalog)) as CatalogDomain; - fs.writeFileSync( - path.join(OUTPUT_DIR, filename), - JSON.stringify(resolved, null, "\t"), - "utf-8", - ); + const filename = `${source.domain}.json`; + activeDomainFiles.add(filename); - manifest.domains[source.domain] = { - file: filename, - title: resolved.title, - endpointCount: resolved.endpoints.length, - }; + fs.writeFileSync( + path.join(OUTPUT_DIR, filename), + JSON.stringify(resolved, null, "\t"), + "utf-8", + ); - totalEndpoints += resolved.endpoints.length; - console.log( - ` ${source.domain}: ${resolved.endpoints.length} endpoints from ${spec.info.title} v${spec.info.version}`, - ); + manifest.domains[source.domain] = { + file: filename, + title: resolved.title, + endpointCount: resolved.endpoints.length, + }; + + totalEndpoints += resolved.endpoints.length; + console.log( + ` ${source.domain}: ${resolved.endpoints.length} endpoints from ${spec.info.title} v${spec.info.version}`, + ); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Error( + `failed processing ${source.repoName} (${source.specVersion}): ${message}`, + ); + } + } + } finally { + if (cloneRoot) { + fs.rmSync(cloneRoot, { recursive: true, force: true }); + } } + removeStaleDomainJsonFiles(activeDomainFiles); + fs.writeFileSync( path.join(OUTPUT_DIR, "manifest.json"), JSON.stringify(manifest, null, "\t"), "utf-8", ); + writeRegistryFile(Object.keys(manifest.domains)); + console.log( `\nGenerated API catalog: ${Object.keys(manifest.domains).length} domains, ${totalEndpoints} endpoints`, ); } -main().catch((err) => { - console.error("Fatal error:", err); +main().catch((error) => { + console.error("Fatal error:", error); process.exit(1); }); diff --git a/src/cli-entry.ts b/src/cli-entry.ts index 0cca914..35cbf42 100644 --- a/src/cli-entry.ts +++ b/src/cli-entry.ts @@ -492,7 +492,12 @@ export function runCli(rawArgv: ReadonlyArray): Promise { const isCliValidation = errorTag !== undefined && CLI_VALIDATION_TAGS.has(errorTag); - let details: { message: string; code: string; fix: string }; + let details: { + message: string; + code: string; + fix: string; + details?: Record; + }; if (isCliValidation) { // biome-ignore lint/suspicious/noExplicitAny: @effect/cli ValidationError is a union @@ -508,14 +513,22 @@ export function runCli(rawArgv: ReadonlyArray): Promise { if (isStreaming) { yield* writer.emitStreamError( cmdStr, - { message: details.message, code: details.code }, + { + message: details.message, + code: details.code, + details: details.details, + }, details.fix, rootNextActions, ); } else { yield* writer.emitError( cmdStr, - { message: details.message, code: details.code }, + { + message: details.message, + code: details.code, + details: details.details, + }, details.fix, rootNextActions, ); diff --git a/src/cli/agent/errors.ts b/src/cli/agent/errors.ts index 48a9733..c739480 100644 --- a/src/cli/agent/errors.ts +++ b/src/cli/agent/errors.ts @@ -1,11 +1,16 @@ import * as HelpDoc from "@effect/cli/HelpDoc"; import type { ValidationError as EffectValidationError } from "@effect/cli/ValidationError"; -import { type CliError, errorCode } from "../../effect/errors"; +import { + type ApiErrorContext, + type CliError, + errorCode, +} from "../../effect/errors"; export interface AgentErrorDetails { message: string; code: string; fix: string; + details?: Record; } const ANSI_ESCAPE_PATTERN = new RegExp( @@ -28,9 +33,68 @@ function formatValidationMessage(error: EffectValidationError): string { return "Invalid command input"; } +function apiDetails(error: CliError): Record | undefined { + const context = error as Partial; + + const details: Record = {}; + if (typeof context.status === "number") { + details.status = context.status; + } + if (typeof context.statusText === "string" && context.statusText.length > 0) { + details.status_text = context.statusText; + } + if (typeof context.endpoint === "string" && context.endpoint.length > 0) { + details.endpoint = context.endpoint; + } + if (typeof context.method === "string" && context.method.length > 0) { + details.method = context.method; + } + if (typeof context.requestId === "string" && context.requestId.length > 0) { + details.request_id = context.requestId; + } + if (context.responseBody !== undefined) { + details.response = context.responseBody; + } + + return Object.keys(details).length > 0 ? details : undefined; +} + +function hasGraphqlErrors( + details: Record | undefined, +): boolean { + const response = details?.response; + if (typeof response !== "object" || response === null) { + return false; + } + + const errors = (response as Record).errors; + return Array.isArray(errors) && errors.length > 0; +} + +function fixForNetworkError( + details: Record | undefined, +): string { + if (hasGraphqlErrors(details)) { + return "Check GraphQL query, variables, and operationName. Inspect error.details.response.errors for resolver/validation details."; + } + + const status = details?.status; + if (typeof status === "number") { + if (status >= 400 && status < 500) { + return "Check request path/query/body. Inspect error.details.response for API validation feedback."; + } + if (status >= 500) { + return "The API is currently failing server-side. Retry, or check service health/incidents."; + } + } + + return "Verify environment connectivity with: godaddy env get and retry."; +} + function fromTaggedError(error: CliError): AgentErrorDetails { const code = errorCode(error); const message = error.userMessage || error.message; + const details = apiDetails(error); switch (error._tag) { case "ValidationError": @@ -44,6 +108,7 @@ function fromTaggedError(error: CliError): AgentErrorDetails { message, code: "AUTH_REQUIRED", fix: "Run: godaddy auth login", + details, }; case "ConfigurationError": return { @@ -55,7 +120,8 @@ function fromTaggedError(error: CliError): AgentErrorDetails { return { message, code, - fix: "Verify environment connectivity with: godaddy env get and retry.", + fix: fixForNetworkError(details), + details, }; case "SecurityError": return { diff --git a/src/cli/agent/stream.ts b/src/cli/agent/stream.ts index 15c678e..8f661c9 100644 --- a/src/cli/agent/stream.ts +++ b/src/cli/agent/stream.ts @@ -45,6 +45,7 @@ export interface StreamErrorEvent { error: { message: string; code: string; + details?: Record; }; fix: string; next_actions: NextAction[]; diff --git a/src/cli/agent/types.ts b/src/cli/agent/types.ts index 60bf5d3..89cacbf 100644 --- a/src/cli/agent/types.ts +++ b/src/cli/agent/types.ts @@ -27,6 +27,7 @@ export interface AgentErrorEnvelope { error: { message: string; code: string; + details?: Record; }; fix: string; next_actions: NextAction[]; diff --git a/src/cli/commands/api.ts b/src/cli/commands/api.ts index 550ab9e..761e3f9 100644 --- a/src/cli/commands/api.ts +++ b/src/cli/commands/api.ts @@ -19,7 +19,7 @@ import { type CatalogDomain, type CatalogEndpoint, findEndpointByAnyMethodEffect, - findEndpointByOperationIdEffect, + findEndpointByPathEffect, listDomainsEffect, loadDomainEffect, searchEndpointsEffect, @@ -35,6 +35,8 @@ const VALID_METHODS: readonly HttpMethod[] = [ "DELETE", ]; +const MAX_GRAPHQL_OPERATION_PREVIEW = 20; + // --------------------------------------------------------------------------- // next_actions helpers // --------------------------------------------------------------------------- @@ -49,8 +51,7 @@ const apiGroupActions: NextAction[] = [ description: "Describe an API endpoint's schema and parameters", params: { endpoint: { - description: - "Operation ID or path (e.g. commerce.location.verify-address or /location/addresses)", + description: "API path (e.g. /location/addresses)", required: true, }, }, @@ -68,7 +69,7 @@ const apiGroupActions: NextAction[] = [ params: { endpoint: { description: - "Relative API endpoint (e.g. /v1/commerce/location/addresses)", + "Relative API endpoint path (e.g. /v1/commerce/location/addresses)", required: true, }, }, @@ -87,7 +88,7 @@ function describeNextActions( ); const callParams: NonNullable = { endpoint: { - description: "Relative API endpoint", + description: "Relative API endpoint path", value: fullPath, required: true, }, @@ -126,8 +127,8 @@ function describeNextActions( description: `Describe ${next.summary}`, params: { endpoint: { - description: "Operation ID or path", - value: next.operationId, + description: "API path", + value: next.path, required: true, }, }, @@ -160,16 +161,16 @@ function listNextActions(firstDomain?: string): NextAction[] { ]; } -function searchNextActions(firstOperationId?: string): NextAction[] { +function searchNextActions(firstPath?: string): NextAction[] { const actions: NextAction[] = []; - if (firstOperationId) { + if (firstPath) { actions.push({ command: "godaddy api describe ", description: "Describe this endpoint", params: { endpoint: { - description: "Operation ID or path", - value: firstOperationId, + description: "API path", + value: firstPath, required: true, }, }, @@ -189,7 +190,7 @@ function callNextActions(): NextAction[] { description: "Call another API endpoint", params: { endpoint: { - description: "Relative API endpoint (e.g. /v1/domains)", + description: "Relative API endpoint path (e.g. /v1/domains)", required: true, }, }, @@ -285,6 +286,216 @@ function tokenHasScopes(token: string, required: string[]): boolean { return required.every((s) => granted.has(s)); } +function isHttpMethod(value: string): value is HttpMethod { + return VALID_METHODS.includes(value as HttpMethod); +} + +const ABSOLUTE_HTTP_URL_PATTERN = /^https?:\/\//i; +const TRUSTED_API_HOSTS = new Set(["api.godaddy.com", "api.ote-godaddy.com"]); + +interface ParsedEndpointInput { + callEndpoint: string; + catalogPathCandidates: string[]; + absoluteUrl: URL | null; + isTrustedAbsolute: boolean; + invalidAbsoluteUrl: boolean; +} + +function parseAbsoluteHttpUrl(value: string): URL | null { + try { + const parsed = new URL(value.trim()); + if (parsed.protocol !== "http:" && parsed.protocol !== "https:") { + return null; + } + return parsed; + } catch { + return null; + } +} + +function normalizeCatalogPath(pathValue: string): string { + const pathOnly = pathValue.split(/[?#]/, 1)[0] || "/"; + const withLeadingSlash = pathOnly.startsWith("/") ? pathOnly : `/${pathOnly}`; + + if (withLeadingSlash.length > 1 && withLeadingSlash.endsWith("/")) { + return withLeadingSlash.slice(0, -1); + } + + return withLeadingSlash; +} + +function buildCatalogPathCandidates(pathValue: string): string[] { + const normalizedPath = normalizeCatalogPath(pathValue); + const candidates = [normalizedPath]; + + const commercePrefixMatch = normalizedPath.match(/^\/v\d+\/commerce(\/.*)$/i); + if (commercePrefixMatch?.[1]) { + candidates.push(commercePrefixMatch[1]); + } + + return [...new Set(candidates)]; +} + +function parseEndpointInput(endpoint: string): ParsedEndpointInput { + const trimmed = endpoint.trim(); + + if (trimmed.length === 0) { + return { + callEndpoint: "/", + catalogPathCandidates: ["/"], + absoluteUrl: null, + isTrustedAbsolute: false, + invalidAbsoluteUrl: false, + }; + } + + if (ABSOLUTE_HTTP_URL_PATTERN.test(trimmed)) { + const absoluteUrl = parseAbsoluteHttpUrl(trimmed); + if (!absoluteUrl) { + return { + callEndpoint: trimmed, + catalogPathCandidates: buildCatalogPathCandidates(trimmed), + absoluteUrl: null, + isTrustedAbsolute: false, + invalidAbsoluteUrl: true, + }; + } + + const isTrustedAbsolute = + absoluteUrl.protocol === "https:" && + TRUSTED_API_HOSTS.has(absoluteUrl.hostname.toLowerCase()); + + const relativePath = `${absoluteUrl.pathname || "/"}${absoluteUrl.search}${absoluteUrl.hash}`; + + return { + callEndpoint: isTrustedAbsolute ? relativePath : trimmed, + catalogPathCandidates: buildCatalogPathCandidates( + absoluteUrl.pathname || "/", + ), + absoluteUrl, + isTrustedAbsolute, + invalidAbsoluteUrl: false, + }; + } + + const callEndpoint = trimmed.startsWith("/") ? trimmed : `/${trimmed}`; + return { + callEndpoint, + catalogPathCandidates: buildCatalogPathCandidates(callEndpoint), + absoluteUrl: null, + isTrustedAbsolute: false, + invalidAbsoluteUrl: false, + }; +} + +function resolveCatalogEndpointEffect( + method: HttpMethod, + methodProvided: boolean, + pathCandidates: string[], + fallbackEndpoint: string, +): Effect.Effect<{ + method: HttpMethod; + endpoint: string; + catalogMatch?: { domain: CatalogDomain; endpoint: CatalogEndpoint }; +}> { + return Effect.gen(function* () { + let catalogMatch: + | { domain: CatalogDomain; endpoint: CatalogEndpoint } + | undefined; + let matchedPath: string | undefined; + + if (methodProvided) { + for (const candidatePath of pathCandidates) { + const byPath = yield* findEndpointByPathEffect(method, candidatePath); + if (Option.isSome(byPath)) { + catalogMatch = byPath.value; + matchedPath = candidatePath; + break; + } + } + } else { + for (const candidatePath of pathCandidates) { + const byAnyMethod = yield* findEndpointByAnyMethodEffect(candidatePath); + if (Option.isSome(byAnyMethod)) { + catalogMatch = byAnyMethod.value; + matchedPath = candidatePath; + break; + } + } + } + + let resolvedMethod = method; + let resolvedEndpoint = fallbackEndpoint; + + if (catalogMatch) { + if (matchedPath === catalogMatch.endpoint.path) { + resolvedEndpoint = buildCallEndpoint( + catalogMatch.domain, + catalogMatch.endpoint, + ); + } + + if (!methodProvided && isHttpMethod(catalogMatch.endpoint.method)) { + resolvedMethod = catalogMatch.endpoint.method; + } + } + + return { + method: resolvedMethod, + endpoint: resolvedEndpoint, + catalogMatch, + }; + }); +} + +function buildCallEndpoint( + domain: CatalogDomain, + endpoint: CatalogEndpoint, +): string { + return `${domain.baseUrl}${endpoint.path}`.replace( + /^https:\/\/api\.godaddy\.com/i, + "", + ); +} + +function summarizeGraphqlSchema(graphql: CatalogEndpoint["graphql"]) { + if (!graphql) return undefined; + + const queryCount = graphql.operations.filter( + (operation) => operation.kind === "query", + ).length; + const mutationCount = graphql.operations.length - queryCount; + + const operationSummaries = graphql.operations.map((operation) => ({ + name: operation.name, + kind: operation.kind, + returnType: operation.returnType, + deprecated: operation.deprecated, + deprecationReason: operation.deprecationReason, + args: operation.args.map((arg) => ({ + name: arg.name, + type: arg.type, + required: arg.required, + defaultValue: arg.defaultValue, + })), + })); + + const shownOperations = operationSummaries.slice( + 0, + MAX_GRAPHQL_OPERATION_PREVIEW, + ); + + return { + schemaRef: graphql.schemaRef, + operationCount: graphql.operationCount, + queryCount, + mutationCount, + operations: shownOperations, + operationsShown: shownOperations.length, + operationsTruncated: operationSummaries.length > shownOperations.length, + }; +} + // --------------------------------------------------------------------------- // Subcommand: api list // --------------------------------------------------------------------------- @@ -322,6 +533,7 @@ const apiList = Command.make( path: e.path, summary: e.summary, scopes: e.scopes, + graphql_operations: e.graphql?.operationCount, })); const truncated = truncateList( @@ -350,8 +562,8 @@ const apiList = Command.make( description: `Describe ${endpointSummaries[0].summary}`, params: { endpoint: { - description: "Operation ID or path", - value: endpointSummaries[0].operationId, + description: "API path", + value: endpointSummaries[0].path, required: true, }, }, @@ -398,26 +610,33 @@ const apiDescribe = Command.make( "describe", { endpoint: Args.text({ name: "endpoint" }).pipe( - Args.withDescription( - "Operation ID (e.g. commerce.location.verify-address) or path (e.g. /location/addresses)", - ), + Args.withDescription("API path (e.g. /location/addresses)"), ), }, ({ endpoint }) => Effect.gen(function* () { const writer = yield* EnvelopeWriter; - // Try to find by operation ID first, then by path - let result = yield* findEndpointByOperationIdEffect(endpoint); + const { catalogPathCandidates: pathCandidates } = + parseEndpointInput(endpoint); - if (Option.isNone(result)) { - // Try as a path, testing all HTTP methods - result = yield* findEndpointByAnyMethodEffect(endpoint); + // Try exact path lookup first + let result: Option.Option<{ + domain: CatalogDomain; + endpoint: CatalogEndpoint; + }> = Option.none(); + + for (const candidatePath of pathCandidates) { + const exactMatch = yield* findEndpointByAnyMethodEffect(candidatePath); + if (Option.isSome(exactMatch)) { + result = exactMatch; + break; + } } // Fallback: fuzzy search if (Option.isNone(result)) { - const searchResults = yield* searchEndpointsEffect(endpoint); + const searchResults = yield* searchEndpointsEffect(pathCandidates[0]); if (searchResults.length === 1) { result = Option.some(searchResults[0]); @@ -441,8 +660,8 @@ const apiDescribe = Command.make( description: `${m.method} ${m.path} — ${m.summary}`, params: { endpoint: { - description: "Operation ID or path", - value: m.operationId, + description: "API path", + value: m.path, required: true, }, }, @@ -480,6 +699,7 @@ const apiDescribe = Command.make( requestBody: ep.requestBody, responses: ep.responses, scopes: ep.scopes, + graphql: summarizeGraphqlSchema(ep.graphql), }, `api-describe-${ep.operationId}`, ); @@ -511,7 +731,7 @@ const apiSearch = Command.make( { query: Args.text({ name: "query" }).pipe( Args.withDescription( - "Search term (matches operation ID, summary, description, path)", + "Search term (matches path, summary, and description)", ), ), }, @@ -527,6 +747,7 @@ const apiSearch = Command.make( summary: r.endpoint.summary, domain: r.domain.name, scopes: r.endpoint.scopes, + graphql_operations: r.endpoint.graphql?.operationCount, })); const truncated = truncateList(items, `api-search-${query}`); @@ -541,7 +762,7 @@ const apiSearch = Command.make( truncated: truncated.metadata.truncated, full_output: truncated.metadata.full_output, }, - searchNextActions(items[0]?.operationId), + searchNextActions(items[0]?.path), ); }), ).pipe(Command.withDescription("Search for API endpoints by keyword")); @@ -555,13 +776,13 @@ const apiCall = Command.make( { endpoint: Args.text({ name: "endpoint" }).pipe( Args.withDescription( - "API endpoint (for example: /v1/commerce/location/addresses)", + "API endpoint path (for example: /v1/commerce/location/addresses)", ), ), method: Options.text("method").pipe( Options.withAlias("X"), Options.withDescription("HTTP method (GET, POST, PUT, PATCH, DELETE)"), - Options.withDefault("GET"), + Options.optional, ), field: Options.text("field").pipe( Options.withAlias("f"), @@ -601,18 +822,60 @@ const apiCall = Command.make( Effect.gen(function* () { const writer = yield* EnvelopeWriter; const cliConfig = yield* CliConfig; - const methodInput = config.method.toUpperCase(); - if (!VALID_METHODS.includes(methodInput as HttpMethod)) { + const methodInput = Option.getOrElse( + config.method, + () => "GET", + ).toUpperCase(); + if (!isHttpMethod(methodInput)) { return yield* Effect.fail( new ValidationError({ - message: `Invalid HTTP method: ${config.method}`, + message: `Invalid HTTP method: ${methodInput}`, userMessage: `Method must be one of: ${VALID_METHODS.join(", ")}`, }), ); } - const method = methodInput as HttpMethod; + const methodProvided = Option.isSome(config.method); + const parsedEndpoint = parseEndpointInput(config.endpoint); + + if (parsedEndpoint.invalidAbsoluteUrl) { + return yield* Effect.fail( + new ValidationError({ + message: `Invalid endpoint URL: ${config.endpoint}`, + userMessage: + "Endpoint must be a valid URL or a relative path (for example: /v1/domains).", + }), + ); + } + + if (parsedEndpoint.absoluteUrl && !parsedEndpoint.isTrustedAbsolute) { + return yield* Effect.fail( + new ValidationError({ + message: `Untrusted endpoint host: ${parsedEndpoint.absoluteUrl.hostname}`, + userMessage: + "Use a relative endpoint path, or a trusted GoDaddy API URL on api.godaddy.com or api.ote-godaddy.com.", + }), + ); + } + + const resolved = yield* resolveCatalogEndpointEffect( + methodInput, + methodProvided, + parsedEndpoint.catalogPathCandidates, + parsedEndpoint.callEndpoint, + ); + + const method = resolved.method; + const resolvedEndpoint = resolved.endpoint; + const catalogMatch = resolved.catalogMatch; + + if (catalogMatch && cliConfig.verbosity >= 1) { + process.stderr.write( + `Resolved endpoint to ${catalogMatch.endpoint.method} ${resolvedEndpoint}\n`, + ); + } + const fields = yield* parseFieldsEffect( normalizeStringArray(config.field), ); @@ -626,20 +889,35 @@ const apiCall = Command.make( body = yield* readBodyFromFileEffect(filePath); } - const requiredScopes = config.scope.flatMap((s) => - s - .split(/[\s,]+/) - .map((t) => t.trim()) - .filter((t) => t.length > 0), + const requiredScopesSet = new Set( + config.scope.flatMap((scopeToken) => + scopeToken + .split(/[\s,]+/) + .map((token) => token.trim()) + .filter((token) => token.length > 0), + ), ); + if (catalogMatch) { + for (const scope of catalogMatch.endpoint.scopes) { + requiredScopesSet.add(scope); + } + } + + const requiredScopes = [...requiredScopesSet]; + + const graphqlRequest = + catalogMatch?.endpoint.graphql !== undefined || + /\/graphql(?:$|[/?#])/i.test(resolvedEndpoint); + const requestOpts = { - endpoint: config.endpoint, + endpoint: resolvedEndpoint, method, fields: Object.keys(fields).length > 0 ? fields : undefined, body, headers: Object.keys(headers).length > 0 ? headers : undefined, debug: cliConfig.verbosity >= 2, + graphql: graphqlRequest, }; // First attempt @@ -725,12 +1003,23 @@ const apiCall = Command.make( yield* writer.emitSuccess( "godaddy api call", { - endpoint: config.endpoint.startsWith("/") - ? config.endpoint - : `/${config.endpoint}`, + endpoint: resolvedEndpoint, method, status: response.status, status_text: response.statusText, + resolved: + catalogMatch === undefined + ? undefined + : { + domain: catalogMatch.domain.name, + path: catalogMatch.endpoint.path, + method: catalogMatch.endpoint.method, + scopes: catalogMatch.endpoint.scopes, + graphql_operations: + catalogMatch.endpoint.graphql?.operationCount, + }, + scopes_requested: + requiredScopes.length > 0 ? requiredScopes : undefined, headers: config.include ? sanitizeResponseHeaders(response.headers) : undefined, @@ -768,8 +1057,8 @@ const apiParent = Command.make("api", {}, () => { command: "godaddy api describe ", description: - "Show detailed schema information for an API endpoint (by operation ID or path)", - usage: "godaddy api describe ", + "Show detailed schema information for an API endpoint (by path)", + usage: "godaddy api describe ", }, { command: "godaddy api search ", @@ -778,7 +1067,7 @@ const apiParent = Command.make("api", {}, () => }, { command: "godaddy api call ", - description: "Make an authenticated API request", + description: "Make an authenticated API request (endpoint path)", usage: "godaddy api call [-X method] [-f field=value] [-F file] [-H header] [-q path] [-i] [-s scope]", }, diff --git a/src/cli/schemas/api/bulk-operations.json b/src/cli/schemas/api/bulk-operations.json new file mode 100644 index 0000000..84abe94 --- /dev/null +++ b/src/cli/schemas/api/bulk-operations.json @@ -0,0 +1,1517 @@ +{ + "name": "bulk-operations", + "title": "Bulk Operations API", + "description": "Bulk Operations API handles the asynchronous processing of any \nbulk action, such as an ingestion or export, across Commerce platform.\n", + "version": "1.0.0", + "baseUrl": "https://api.godaddy.com/v1/commerce", + "endpoints": [ + { + "operationId": "get_stores__storeId__bulk_ingestions", + "method": "GET", + "path": "/stores/{storeId}/bulk-ingestions", + "summary": "Get bulk ingestions", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "The Store ID", + "schema": { + "$ref": "./common-types/v1/schemas/json/uuid.json" + } + }, + { + "name": "filter", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "pageSize", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "minimum": 1 + } + }, + { + "name": "pageToken", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Get bulk ingestions", + "schema": { + "title": "Bulk Ingestions", + "description": "A list of bulk ingestions", + "type": "object", + "properties": { + "bulkIngestions": { + "type": "array", + "description": "The bulk ingestions", + "items": { + "$ref": "./models/Bulkingestion.yaml" + } + }, + "pageToken": { + "type": "string", + "description": "The page token" + }, + "links": { + "type": "array", + "description": "The links", + "items": { + "$ref": "./common-types/v1/schemas/json/link-description.json" + } + } + } + } + }, + "404": { + "description": "See #/components/responses/Error" + }, + "500": { + "description": "See #/components/responses/InternalServerError" + } + }, + "scopes": [] + }, + { + "operationId": "post_stores__storeId__bulk_ingestions", + "method": "POST", + "path": "/stores/{storeId}/bulk-ingestions", + "summary": "Create bulk ingestion", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "The Store ID", + "schema": { + "$ref": "./common-types/v1/schemas/json/uuid.json" + } + } + ], + "requestBody": { + "required": false, + "contentType": "application/json", + "schema": { + "title": "Create Bulk Ingestion", + "description": "Create bulk ingestion payload", + "type": "object", + "properties": { + "entityType": { + "type": "string" + }, + "intent": { + "type": "string" + } + } + } + }, + "responses": { + "201": { + "description": "Create bulk ingestion", + "schema": { + "title": "Bulk Ingestion", + "description": "A bulk ingestion object", + "type": "object", + "properties": { + "bulkIngestion": { + "$ref": "./models/Bulkingestion.yaml" + }, + "links": { + "type": "array", + "items": { + "$ref": "./common-types/v1/schemas/json/link-description.json" + } + } + } + } + }, + "400": { + "description": "See #/components/responses/Error" + }, + "500": { + "description": "See #/components/responses/InternalServerError" + } + }, + "scopes": [] + }, + { + "operationId": "get_stores__storeId__bulk_ingestions__bulkIngestionId_", + "method": "GET", + "path": "/stores/{storeId}/bulk-ingestions/{bulkIngestionId}", + "summary": "Get bulk ingestion", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "The Store ID", + "schema": { + "$ref": "./common-types/v1/schemas/json/uuid.json" + } + }, + { + "name": "bulkIngestionId", + "in": "path", + "required": true, + "description": "The Bulk Ingestion ID", + "schema": { + "$ref": "./common-types/v1/schemas/json/uuid.json" + } + } + ], + "responses": { + "200": { + "description": "Get bulk ingestion", + "schema": { + "$ref": "./models/Bulkingestion.yaml" + } + }, + "404": { + "description": "See #/components/responses/Error" + }, + "500": { + "description": "See #/components/responses/InternalServerError" + } + }, + "scopes": [] + }, + { + "operationId": "patch_stores__storeId__bulk_ingestions__bulkIngestionId_", + "method": "PATCH", + "path": "/stores/{storeId}/bulk-ingestions/{bulkIngestionId}", + "summary": "Update bulk ingestion", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "The Store ID", + "schema": { + "$ref": "./common-types/v1/schemas/json/uuid.json" + } + }, + { + "name": "bulkIngestionId", + "in": "path", + "required": true, + "description": "The Bulk Ingestion ID", + "schema": { + "$ref": "./common-types/v1/schemas/json/uuid.json" + } + } + ], + "requestBody": { + "required": false, + "contentType": "application/json", + "schema": { + "type": "object", + "title": "Update Bulk Ingestion", + "description": "Update bulk ingestion payload", + "properties": { + "mappedAttributes": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "pointer": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ] + } + } + } + }, + "overrides": { + "$ref": "./models/BulkIngestionOverride.yaml" + } + } + } + }, + "responses": { + "200": { + "description": "Update bulk ingestion", + "schema": { + "$ref": "./models/Bulkingestion.yaml" + } + }, + "400": { + "description": "See #/components/responses/Error" + }, + "404": { + "description": "See #/components/responses/Error" + }, + "500": { + "description": "See #/components/responses/InternalServerError" + } + }, + "scopes": [] + }, + { + "operationId": "delete_stores__storeId__bulk_ingestions__bulkIngestionId_", + "method": "DELETE", + "path": "/stores/{storeId}/bulk-ingestions/{bulkIngestionId}", + "summary": "Delete bulk ingestion", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "The Store ID", + "schema": { + "$ref": "./common-types/v1/schemas/json/uuid.json" + } + }, + { + "name": "bulkIngestionId", + "in": "path", + "required": true, + "description": "The Bulk Ingestion ID", + "schema": { + "$ref": "./common-types/v1/schemas/json/uuid.json" + } + } + ], + "responses": { + "204": { + "description": "Bulk ingestion deleted successfully" + }, + "400": { + "description": "See #/components/responses/Error" + }, + "500": { + "description": "See #/components/responses/InternalServerError" + } + }, + "scopes": [] + }, + { + "operationId": "post_stores__storeId__bulk_ingestions__bulkIngestionId__validations", + "method": "POST", + "path": "/stores/{storeId}/bulk-ingestions/{bulkIngestionId}/validations", + "summary": "Validate bulk ingestion", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "The Store ID", + "schema": { + "$ref": "./common-types/v1/schemas/json/uuid.json" + } + }, + { + "name": "bulkIngestionId", + "in": "path", + "required": true, + "description": "The Bulk Ingestion ID", + "schema": { + "$ref": "./common-types/v1/schemas/json/uuid.json" + } + } + ], + "responses": { + "200": { + "description": "Bulk ingestion validated", + "schema": { + "$ref": "./models/Bulkingestion.yaml" + } + }, + "400": { + "description": "See #/components/responses/Error" + }, + "404": { + "description": "See #/components/responses/Error" + }, + "500": { + "description": "See #/components/responses/InternalServerError" + } + }, + "scopes": [] + }, + { + "operationId": "post_stores__storeId__bulk_ingestions__bulkIngestionId__items__index__validations", + "method": "POST", + "path": "/stores/{storeId}/bulk-ingestions/{bulkIngestionId}/items/{index}/validations", + "summary": "Validate bulk ingestion by index", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "The Store ID", + "schema": { + "$ref": "./common-types/v1/schemas/json/uuid.json" + } + }, + { + "name": "bulkIngestionId", + "in": "path", + "required": true, + "description": "The Bulk Ingestion ID", + "schema": { + "$ref": "./common-types/v1/schemas/json/uuid.json" + } + }, + { + "name": "override index", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Bulk ingestion validated", + "schema": { + "$ref": "./models/BulkIngestion.yaml" + } + }, + "400": { + "description": "See #/components/responses/Error" + }, + "404": { + "description": "See #/components/responses/Error" + }, + "500": { + "description": "See #/components/responses/InternalServerError" + } + }, + "scopes": [] + }, + { + "operationId": "post_stores__storeId__bulk_ingestions__bulkIngestionId__executions", + "method": "POST", + "path": "/stores/{storeId}/bulk-ingestions/{bulkIngestionId}/executions", + "summary": "Execute bulk ingestion", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "The Store ID", + "schema": { + "$ref": "./common-types/v1/schemas/json/uuid.json" + } + }, + { + "name": "bulkIngestionId", + "in": "path", + "required": true, + "description": "The Bulk Ingestion ID", + "schema": { + "$ref": "./common-types/v1/schemas/json/uuid.json" + } + } + ], + "responses": { + "200": { + "description": "Bulk ingestion executed", + "schema": { + "$ref": "./models/Bulkingestion.yaml" + } + }, + "400": { + "description": "See #/components/responses/Error" + }, + "404": { + "description": "See #/components/responses/Error" + }, + "500": { + "description": "See #/components/responses/InternalServerError" + } + }, + "scopes": [] + }, + { + "operationId": "post_stores__storeId__bulk_ingestions__bulkIngestionId__cancelations", + "method": "POST", + "path": "/stores/{storeId}/bulk-ingestions/{bulkIngestionId}/cancelations", + "summary": "Cancel bulk ingestion", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "The Store ID", + "schema": { + "$ref": "./common-types/v1/schemas/json/uuid.json" + } + }, + { + "name": "bulkIngestionId", + "in": "path", + "required": true, + "description": "The Bulk Ingestion ID", + "schema": { + "$ref": "./common-types/v1/schemas/json/uuid.json" + } + } + ], + "responses": { + "200": { + "description": "Bulk ingestion cancelled", + "schema": { + "$ref": "./models/Bulkingestion.yaml" + } + }, + "400": { + "description": "See #/components/responses/Error" + }, + "404": { + "description": "See #/components/responses/Error" + }, + "500": { + "description": "See #/components/responses/InternalServerError" + } + }, + "scopes": [] + }, + { + "operationId": "get_stores__storeId__bulk_ingestions__bulkIngestionId__items", + "method": "GET", + "path": "/stores/{storeId}/bulk-ingestions/{bulkIngestionId}/items", + "summary": "Get bulk ingestion items", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "The Store ID", + "schema": { + "$ref": "./common-types/v1/schemas/json/uuid.json" + } + }, + { + "name": "bulkIngestionId", + "in": "path", + "required": true, + "description": "The Bulk Ingestion ID", + "schema": { + "$ref": "./common-types/v1/schemas/json/uuid.json" + } + } + ], + "responses": { + "200": { + "description": "Get bulk ingestion items", + "schema": { + "type": "object", + "title": "Bulk Ingestion Items", + "description": "A list of bulk ingestion items", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "./models/BulkIngestionItem.yaml" + } + }, + "pageToken": { + "type": "string" + }, + "links": { + "type": "array", + "items": { + "$ref": "./common-types/v1/schemas/json/link-description.json" + } + } + } + } + }, + "404": { + "description": "See #/components/responses/Error" + }, + "500": { + "description": "See #/components/responses/InternalServerError" + } + }, + "scopes": [] + }, + { + "operationId": "get_stores__storeId__bulk_ingestions__bulkIngestionId__errors", + "method": "GET", + "path": "/stores/{storeId}/bulk-ingestions/{bulkIngestionId}/errors", + "summary": "Get bulk ingestion errors", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "The Store ID", + "schema": { + "$ref": "./common-types/v1/schemas/json/uuid.json" + } + }, + { + "name": "bulkIngestionId", + "in": "path", + "required": true, + "description": "The Bulk Ingestion ID", + "schema": { + "$ref": "./common-types/v1/schemas/json/uuid.json" + } + } + ], + "responses": { + "200": { + "description": "Get bulk ingestion errors", + "schema": { + "type": "object", + "title": "Bulk Ingestion Errors", + "description": "A list of bulk ingestion errors", + "properties": { + "errors": { + "type": "array", + "items": { + "$ref": "./common-types/v1/schemas/json/error.json" + } + }, + "pageToken": { + "type": "string" + }, + "links": { + "type": "array", + "items": { + "$ref": "./common-types/v1/schemas/json/link-description.json" + } + } + } + } + }, + "404": { + "description": "See #/components/responses/Error" + }, + "500": { + "description": "See #/components/responses/InternalServerError" + } + }, + "scopes": [] + }, + { + "operationId": "get_stores__storeId__bulk_ingestions__bulkIngestionId__overrides", + "method": "GET", + "path": "/stores/{storeId}/bulk-ingestions/{bulkIngestionId}/overrides", + "summary": "Get bulk ingestion overrides", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "The Store ID", + "schema": { + "$ref": "./common-types/v1/schemas/json/uuid.json" + } + }, + { + "name": "bulkIngestionId", + "in": "path", + "required": true, + "description": "The Bulk Ingestion ID", + "schema": { + "$ref": "./common-types/v1/schemas/json/uuid.json" + } + } + ], + "responses": { + "200": { + "description": "Get bulk ingestion overrides", + "schema": { + "type": "object", + "title": "Bulk Ingestion Overrides", + "description": "A list of bulk ingestion overrides", + "properties": { + "overrides": { + "type": "array", + "items": { + "$ref": "./models/BulkIngestionOverride.yaml" + } + }, + "pageToken": { + "type": "string" + }, + "links": { + "type": "array", + "items": { + "$ref": "./common-types/v1/schemas/json/link-description.json" + } + } + } + } + }, + "404": { + "description": "See #/components/responses/Error" + }, + "500": { + "description": "See #/components/responses/InternalServerError" + } + }, + "scopes": [] + }, + { + "operationId": "put_stores__storeId__bulk_ingestions__bulkIngestionId__overrides", + "method": "PUT", + "path": "/stores/{storeId}/bulk-ingestions/{bulkIngestionId}/overrides", + "summary": "Upsert an override", + "description": "Update an existing override or insert a new one if \nit does not already exist. Body must include a JSON\npatch object.\n", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "The Store ID", + "schema": { + "$ref": "./common-types/v1/schemas/json/uuid.json" + } + }, + { + "name": "bulkIngestionId", + "in": "path", + "required": true, + "description": "The Bulk Ingestion ID", + "schema": { + "$ref": "./common-types/v1/schemas/json/uuid.json" + } + } + ], + "requestBody": { + "required": false, + "contentType": "application/json", + "schema": { + "$ref": "#/components/schemas/JSONPatch", + "required": true + } + }, + "responses": { + "200": { + "description": "Override upserted successfully", + "schema": { + "$ref": "./models/BulkIngestionOverride.yaml" + } + }, + "400": { + "description": "See #/components/responses/Error" + }, + "404": { + "description": "See #/components/responses/Error" + }, + "500": { + "description": "See #/components/responses/InternalServerError" + } + }, + "scopes": [] + }, + { + "operationId": "get_stores__storeId__bulk_ingestions__bulkIngestionId__overrides__index_", + "method": "GET", + "path": "/stores/{storeId}/bulk-ingestions/{bulkIngestionId}/overrides/{index}", + "summary": "Get overrides by index", + "description": "Get a list of overrides based on its index.\n", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "The Store ID", + "schema": { + "$ref": "./common-types/v1/schemas/json/uuid.json" + } + }, + { + "name": "bulkIngestionId", + "in": "path", + "required": true, + "description": "The Bulk Ingestion ID", + "schema": { + "$ref": "./common-types/v1/schemas/json/uuid.json" + } + }, + { + "name": "override index", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "includes", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Overrides retrieved", + "schema": { + "type": "object", + "title": "Bulk Ingestion Overrides", + "description": "A list of bulk ingestion overrides", + "properties": { + "entries": { + "type": "array", + "items": { + "$ref": "./models/BulkIngestionOverride.yaml" + } + } + } + } + }, + "404": { + "description": "See #/components/responses/Error" + }, + "500": { + "description": "See #/components/responses/InternalServerError" + } + }, + "scopes": [] + }, + { + "operationId": "delete_stores__storeId__bulk_ingestions__bulkIngestionId__overrides__index_", + "method": "DELETE", + "path": "/stores/{storeId}/bulk-ingestions/{bulkIngestionId}/overrides/{index}", + "summary": "Delete overrides by index", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "The Store ID", + "schema": { + "$ref": "./common-types/v1/schemas/json/uuid.json" + } + }, + { + "name": "bulkIngestionId", + "in": "path", + "required": true, + "description": "The Bulk Ingestion ID", + "schema": { + "$ref": "./common-types/v1/schemas/json/uuid.json" + } + }, + { + "name": "override index", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Overrides deleted", + "schema": { + "type": "object", + "properties": {} + } + }, + "400": { + "description": "See #/components/responses/Error" + }, + "500": { + "description": "See #/components/responses/InternalServerError" + } + }, + "scopes": [] + }, + { + "operationId": "post_stores__storeId__bulk_ingestions__bulkIngestionId__override_batches", + "method": "POST", + "path": "/stores/{storeId}/bulk-ingestions/{bulkIngestionId}/override-batches", + "summary": "Create, update, or delete a batch of overrides", + "description": "Create, update, or delete a batch of overrides for a bulk ingestion", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "The Store ID", + "schema": { + "$ref": "./common-types/v1/schemas/json/uuid.json" + } + }, + { + "name": "bulkIngestionId", + "in": "path", + "required": true, + "description": "The Bulk Ingestion ID", + "schema": { + "$ref": "./common-types/v1/schemas/json/uuid.json" + } + } + ], + "requestBody": { + "required": false, + "contentType": "application/json", + "schema": { + "type": "object", + "properties": { + "action": { + "type": "string", + "description": "The action to perform on the overrides, currently accepts \nPUT or DELETE\n" + }, + "overrides": { + "type": "array", + "items": { + "$ref": "#/components/schemas/JSONPatch" + } + } + } + } + }, + "responses": { + "201": { + "description": "Override Batch PUT Success", + "schema": { + "type": "object", + "properties": { + "overrides": { + "type": "array", + "items": { + "$ref": "./models/BulkIngestionOverride.yaml" + } + }, + "errors": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, + "204": { + "description": "Override Batch DELETE Success", + "schema": { + "type": "object", + "properties": { + "errors": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, + "422": { + "description": "See #/components/responses/Error" + }, + "500": { + "description": "See #/components/responses/InternalServerError" + } + }, + "scopes": [] + }, + { + "operationId": "get_v1_commerce_stores__storeId__bulk_operations", + "method": "GET", + "path": "/v1/commerce/stores/{storeId}/bulk-operations", + "summary": "Get bulk operations", + "description": "Get bulk operations of a store", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "The Store ID", + "schema": { + "$ref": "./common-types/v1/schemas/json/uuid.json" + } + } + ], + "responses": { + "200": { + "description": "Successfully retrieved bulk operations", + "schema": { + "type": "object", + "properties": { + "bulkOperation": { + "type": "array", + "description": "The bulk operations", + "items": { + "$ref": "./models/BulkOperation.yaml" + } + } + } + } + }, + "400": { + "description": "See #/components/responses/Error" + }, + "500": { + "description": "See #/components/responses/InternalServerError" + } + }, + "scopes": [] + }, + { + "operationId": "post_v1_commerce_stores__storeId__bulk_operations", + "method": "POST", + "path": "/v1/commerce/stores/{storeId}/bulk-operations", + "summary": "Create a bulk operation", + "description": "Create a new bulk operation", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "The Store ID", + "schema": { + "$ref": "./common-types/v1/schemas/json/uuid.json" + } + } + ], + "requestBody": { + "required": true, + "contentType": "application/json", + "schema": { + "type": "object", + "properties": { + "href": { + "type": "string", + "description": "The URL of the bulk operation", + "example": "/v1/commerce/stores/:storeId/products/:productId" + }, + "method": { + "type": "string", + "description": "The method of the bulk operation", + "example": "PATCH" + }, + "filter": { + "type": "string", + "description": "The optional filter of the bulk operation, should follow\n[OData URL Convention for filtering](http://docs.oasis-open.org/odata/odata/v4.01/cs01/part2-url-conventions/odata-v4.01-cs01-part2-url-conventions.html#sec_SystemQueryOptionfilter)\n", + "example": "status eq 'IDLE'" + }, + "query": { + "type": "string", + "description": "The optional query of the bulk operation", + "example": "type=PHYSICAL" + }, + "body": { + "type": "object", + "description": "The body of the bulk operation request", + "example": { + "name": "Product 1" + } + } + } + } + }, + "responses": { + "201": { + "description": "Bulk operation created", + "schema": { + "type": "object", + "properties": { + "bulkOperation": { + "$ref": "./models/BulkOperation.yaml" + } + } + } + }, + "400": { + "description": "See #/components/responses/Error" + }, + "500": { + "description": "See #/components/responses/InternalServerError" + } + }, + "scopes": [] + }, + { + "operationId": "get_v1_commerce_stores__storeId__bulk_operations__bulkOperationId_", + "method": "GET", + "path": "/v1/commerce/stores/{storeId}/bulk-operations/{bulkOperationId}", + "summary": "Get a bulk operation", + "description": "Get a bulk operation by ID", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "The Store ID", + "schema": { + "$ref": "./common-types/v1/schemas/json/uuid.json" + } + }, + { + "name": "bulkOperationId", + "in": "path", + "required": true, + "description": "The Bulk Operation ID", + "schema": { + "$ref": "./common-types/v1/schemas/json/uuid.json" + } + } + ], + "responses": { + "200": { + "description": "Successfully retrieved bulk operation", + "schema": { + "type": "object", + "properties": { + "bulkOperation": { + "$ref": "./models/BulkOperation.yaml" + } + } + } + }, + "400": { + "description": "See #/components/responses/Error" + }, + "500": { + "description": "See #/components/responses/InternalServerError" + } + }, + "scopes": [] + }, + { + "operationId": "delete_v1_commerce_stores__storeId__bulk_operations__bulkOperationId_", + "method": "DELETE", + "path": "/v1/commerce/stores/{storeId}/bulk-operations/{bulkOperationId}", + "summary": "Cancel a bulk operation", + "description": "Cancel an ongoing bulk operation", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "The Store ID", + "schema": { + "$ref": "./common-types/v1/schemas/json/uuid.json" + } + }, + { + "name": "bulkOperationId", + "in": "path", + "required": true, + "description": "The Bulk Operation ID", + "schema": { + "$ref": "./common-types/v1/schemas/json/uuid.json" + } + } + ], + "responses": { + "204": { + "description": "Bulk operation cancelled", + "schema": { + "type": "object", + "properties": { + "bulkOperation": { + "$ref": "./models/BulkOperation.yaml" + } + } + } + }, + "400": { + "description": "See #/components/responses/Error" + }, + "500": { + "description": "See #/components/responses/InternalServerError" + } + }, + "scopes": [] + }, + { + "operationId": "get_v1_commerce_stores__storeId__bulk_operations__bulkOperationId__errors", + "method": "GET", + "path": "/v1/commerce/stores/{storeId}/bulk-operations/{bulkOperationId}/errors", + "summary": "Get bulk operation errors", + "description": "Get errors during a bulk operation", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "The Store ID", + "schema": { + "$ref": "./common-types/v1/schemas/json/uuid.json" + } + }, + { + "name": "bulkOperationId", + "in": "path", + "required": true, + "description": "The Bulk Operation ID", + "schema": { + "$ref": "./common-types/v1/schemas/json/uuid.json" + } + }, + { + "name": "pageSize", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "minimum": 1 + } + }, + { + "name": "pageToken", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Successfully retrieved bulk operation errors", + "schema": { + "type": "object", + "properties": { + "errors": { + "type": "array", + "items": { + "$ref": "./models/BulkOperationItem.yaml" + } + }, + "pageToken": { + "type": "string", + "description": "The page token" + }, + "links": { + "type": "array", + "description": "The links", + "items": { + "$ref": "./common-types/v1/schemas/json/link-description.json" + } + } + } + } + }, + "400": { + "description": "See #/components/responses/Error" + }, + "500": { + "description": "See #/components/responses/InternalServerError" + } + }, + "scopes": [] + }, + { + "operationId": "get_v1_commerce_stores__storeId__bulk_exports", + "method": "GET", + "path": "/v1/commerce/stores/{storeId}/bulk-exports", + "summary": "Get bulk exports", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "The Store ID", + "schema": { + "$ref": "./common-types/v1/schemas/json/uuid.json" + } + }, + { + "name": "filter", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "pageSize", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "minimum": 1 + } + }, + { + "name": "pageToken", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Get bulk exports", + "schema": { + "title": "Bulk Exports", + "description": "A list of bulk exports", + "type": "object", + "properties": { + "bulkExports": { + "type": "array", + "description": "The bulk exports", + "items": { + "$ref": "./models/BulkExport.yaml" + } + }, + "pageToken": { + "type": "string", + "description": "The page token" + }, + "links": { + "type": "array", + "description": "The links", + "items": { + "$ref": "./common-types/v1/schemas/json/link-description.json" + } + } + } + } + }, + "404": { + "description": "See #/components/responses/Error" + }, + "500": { + "description": "See #/components/responses/InternalServerError" + } + }, + "scopes": [] + }, + { + "operationId": "post_v1_commerce_stores__storeId__bulk_exports", + "method": "POST", + "path": "/v1/commerce/stores/{storeId}/bulk-exports", + "summary": "Create bulk export", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "The Store ID", + "schema": { + "$ref": "./common-types/v1/schemas/json/uuid.json" + } + } + ], + "requestBody": { + "required": false, + "contentType": "application/json", + "schema": { + "$ref": "./models/BulkExport.yaml" + } + }, + "responses": { + "201": { + "description": "Create bulk export", + "schema": { + "title": "Bulk Export", + "description": "A bulk export object", + "type": "object", + "properties": { + "bulkExport": { + "$ref": "./models/BulkExport.yaml" + }, + "links": { + "type": "array", + "description": "Links to the newly created bulk export.", + "items": { + "$ref": "./common-types/v1/schemas/json/link-description.json" + } + } + } + } + }, + "400": { + "description": "See #/components/responses/Error" + }, + "500": { + "description": "See #/components/responses/InternalServerError" + } + }, + "scopes": [] + }, + { + "operationId": "get_v1_commerce_stores__storeId__bulk_exports__bulkExportId_", + "method": "GET", + "path": "/v1/commerce/stores/{storeId}/bulk-exports/{bulkExportId}", + "summary": "Get bulk export", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "The Store ID", + "schema": { + "$ref": "./common-types/v1/schemas/json/uuid.json" + } + }, + { + "name": "bulkExportId", + "in": "path", + "required": true, + "description": "The Bulk Export ID", + "schema": { + "$ref": "./common-types/v1/schemas/json/uuid.json" + } + } + ], + "responses": { + "200": { + "description": "Get bulk export", + "schema": { + "bulkExport": { + "$ref": "./models/BulkExport.yaml" + }, + "links": { + "type": "array", + "description": "Links to self and, if available, next step.", + "items": { + "$ref": "./common-types/v1/schemas/json/link-description.json" + } + } + } + }, + "404": { + "description": "See #/components/responses/Error" + }, + "500": { + "description": "See #/components/responses/InternalServerError" + } + }, + "scopes": [] + }, + { + "operationId": "post_v1_commerce_stores__storeId__bulk_exports__bulkExportId__cancelations", + "method": "POST", + "path": "/v1/commerce/stores/{storeId}/bulk-exports/{bulkExportId}/cancelations", + "summary": "Cancel bulk export", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "The Store ID", + "schema": { + "$ref": "./common-types/v1/schemas/json/uuid.json" + } + }, + { + "name": "bulkExportId", + "in": "path", + "required": true, + "description": "The Bulk Export ID", + "schema": { + "$ref": "./common-types/v1/schemas/json/uuid.json" + } + } + ], + "responses": { + "200": { + "description": "Bulk export cancelled", + "schema": { + "bulkExport": { + "$ref": "./models/BulkExport.yaml" + }, + "links": { + "type": "array", + "description": "Link to the cancelled bulk export.", + "items": { + "$ref": "./common-types/v1/schemas/json/link-description.json" + } + } + } + }, + "400": { + "description": "See #/components/responses/Error" + }, + "404": { + "description": "See #/components/responses/Error" + }, + "500": { + "description": "See #/components/responses/InternalServerError" + } + }, + "scopes": [] + }, + { + "operationId": "get_v1_commerce_stores__storeId__bulk_exports__bulkExportId__downloads", + "method": "GET", + "path": "/v1/commerce/stores/{storeId}/bulk-exports/{bulkExportId}/downloads", + "summary": "Get bulk export downloads", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "The Store ID", + "schema": { + "$ref": "./common-types/v1/schemas/json/uuid.json" + } + }, + { + "name": "bulkExportId", + "in": "path", + "required": true, + "description": "The Bulk Export ID", + "schema": { + "$ref": "./common-types/v1/schemas/json/uuid.json" + } + }, + { + "name": "exportType", + "in": "query", + "required": true, + "description": "The export type, required to get bulk export downloads", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Bulk export with download links", + "schema": { + "bulkExport": { + "$ref": "./models/BulkExport.yaml" + }, + "links": { + "type": "array", + "description": "Links to self and download.", + "items": { + "$ref": "./common-types/v1/schemas/json/link-description.json" + } + } + } + }, + "400": { + "description": "See #/components/responses/Error" + }, + "404": { + "description": "See #/components/responses/Error" + }, + "500": { + "description": "See #/components/responses/InternalServerError" + } + }, + "scopes": [] + } + ] +} diff --git a/src/cli/schemas/api/businesses.json b/src/cli/schemas/api/businesses.json new file mode 100644 index 0000000..b0cc4e0 --- /dev/null +++ b/src/cli/schemas/api/businesses.json @@ -0,0 +1,479 @@ +{ + "name": "businesses", + "title": "Commerce Business API", + "description": "This API is capable of creating, updating, deleting and fetching businesses based on parameters such as a businessId and attributeName.", + "version": "2.1.0", + "baseUrl": "https://api.godaddy.com/v2/commerce", + "endpoints": [ + { + "operationId": "getBusinesses", + "method": "GET", + "path": "/businesses", + "summary": "Get all businesses", + "description": "This endpoint can be used to obtain a list of all the available bussinesses for a specific organization.", + "parameters": [ + { + "name": "X-Request-Id", + "in": "header", + "required": false, + "description": "Request ID", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "page", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "default": 1 + } + }, + { + "name": "pageSize", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "default": 10 + } + }, + { + "name": "totalRequired", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "default": false + } + }, + { + "name": "includeFields", + "in": "query", + "required": false, + "description": "This is the list of fields that will be included in the response", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "If-Modified-Since", + "in": "header", + "required": false, + "description": "Check if the data has been modified since a given date", + "schema": { + "type": "string", + "format": "date-time" + } + } + ], + "responses": { + "200": { + "description": "Business List", + "schema": { + "$ref": "./models/BusinessList.yaml" + } + }, + "default": { + "description": "See #/components/responses/error" + } + }, + "scopes": ["commerce.business:read"] + }, + { + "operationId": "createBusiness", + "method": "POST", + "path": "/businesses", + "summary": "Create a new business", + "description": "Create a new business", + "parameters": [ + { + "name": "X-Request-Id", + "in": "header", + "required": false, + "description": "Request ID", + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "required": false, + "contentType": "application/json", + "schema": { + "$ref": "./models/Business.yaml" + } + }, + "responses": { + "200": { + "description": "Business Created", + "schema": { + "$ref": "./models/Business.yaml" + } + }, + "default": { + "description": "See #/components/responses/error" + } + }, + "scopes": ["commerce.business:create"] + }, + { + "operationId": "getBusinessById", + "method": "GET", + "path": "/businesses/{businessId}", + "summary": "Get a business by businessId", + "description": "This endpoint can be used to retrieve the information of a specific business using the Business ID.", + "parameters": [ + { + "name": "businessId", + "in": "path", + "required": true, + "schema": { + "$ref": "./common-types/v1/uuid.json" + } + }, + { + "name": "X-Request-Id", + "in": "header", + "required": false, + "description": "Request ID", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "includeFields", + "in": "query", + "required": false, + "description": "This is the list of fields that will be included in the response", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "storeDeviceId", + "in": "query", + "required": false, + "description": "Store Device ID", + "schema": { + "type": "string" + } + }, + { + "name": "ignoreDeactivatedDevices", + "in": "query", + "required": false, + "description": "Ignore any deactivated devices when applying this filter", + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Business Found", + "schema": { + "$ref": "./models/Business.yaml" + } + }, + "default": { + "description": "See #/components/responses/error" + } + }, + "scopes": ["commerce.business:read"] + }, + { + "operationId": "patchBusiness", + "method": "PATCH", + "path": "/businesses/{businessId}", + "summary": "Update business information", + "description": "Update business information details using the Business ID.", + "parameters": [ + { + "name": "businessId", + "in": "path", + "required": true, + "schema": { + "$ref": "./common-types/v1/uuid.json" + } + }, + { + "name": "X-Request-Id", + "in": "header", + "required": false, + "description": "Request ID", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "includeFields", + "in": "query", + "required": false, + "description": "This is the list of fields that will be included in the response", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "storeDeviceId", + "in": "query", + "required": false, + "description": "Store Device ID", + "schema": { + "type": "string" + } + }, + { + "name": "ignoreDeactivatedDevices", + "in": "query", + "required": false, + "description": "Ignore any deactivated devices when applying this filter", + "schema": { + "type": "boolean", + "default": true + } + } + ], + "requestBody": { + "required": false, + "contentType": "application/json", + "schema": { + "items": { + "$ref": "./common-types/v1/patch-request.json" + }, + "type": "array" + } + }, + "responses": { + "200": { + "description": "Business updated", + "schema": { + "$ref": "./models/Business.yaml" + } + }, + "404": { + "description": "Business Not Found" + }, + "default": { + "description": "See #/components/responses/error" + } + }, + "scopes": ["commerce.channels:write"] + }, + { + "operationId": "getBusinessAttributes", + "method": "GET", + "path": "/businesses/{businessId}/attributes", + "summary": "Get business attributes", + "description": "Retrieve the attributes of a specific Business using the business ID.", + "parameters": [ + { + "name": "businessId", + "in": "path", + "required": true, + "schema": { + "$ref": "./common-types/v1/uuid.json" + } + }, + { + "name": "X-Request-Id", + "in": "header", + "required": false, + "description": "Request ID", + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Business Attributes Found", + "schema": { + "$ref": "./types/Dictionary.yaml" + } + }, + "default": { + "description": "See #/components/responses/error" + } + }, + "scopes": ["commerce.business:read"] + }, + { + "operationId": "updateBusinessAttribute", + "method": "PUT", + "path": "/businesses/{businessId}/attributes/{attrName}", + "summary": "Update business attribute", + "description": "Update a single business attribute using the attribute name.", + "parameters": [ + { + "name": "X-Request-Id", + "in": "header", + "required": false, + "description": "Request ID", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "businessId", + "in": "path", + "required": true, + "schema": { + "$ref": "./common-types/v1/uuid.json" + } + }, + { + "name": "attrName", + "in": "path", + "required": true, + "description": "Attribute Name", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": false, + "contentType": "application/json", + "schema": { + "$ref": "#/components/schemas/attributeUpdateRequest" + } + }, + "responses": { + "200": { + "description": "Business Attribute updated" + }, + "404": { + "description": "See #/components/responses/error" + }, + "default": { + "description": "See #/components/responses/error" + } + }, + "scopes": ["commerce.business:update"] + }, + { + "operationId": "getBusinessAttribute", + "method": "GET", + "path": "/businesses/{businessId}/attributes/{attrName}", + "summary": "Get business attribute", + "description": "Retrieve the information of a single business attribute using the business and attribute names.", + "parameters": [ + { + "name": "X-Request-Id", + "in": "header", + "required": false, + "description": "Request ID", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "businessId", + "in": "path", + "required": true, + "schema": { + "$ref": "./common-types/v1/uuid.json" + } + }, + { + "name": "attrName", + "in": "path", + "required": true, + "description": "Attribute Name", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Business Attribute Found" + }, + "404": { + "description": "See #/components/responses/error" + }, + "default": { + "description": "See #/components/responses/error" + } + }, + "scopes": ["commerce.business:read"] + }, + { + "operationId": "deleteBusinessAttribute", + "method": "DELETE", + "path": "/businesses/{businessId}/attributes/{attrName}", + "summary": "Delete a business attribute", + "description": "Delete a business attribute using the business ID and attribute name.", + "parameters": [ + { + "name": "X-Request-Id", + "in": "header", + "required": false, + "description": "Request ID", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "businessId", + "in": "path", + "required": true, + "schema": { + "$ref": "./common-types/v1/uuid.json" + } + }, + { + "name": "attrName", + "in": "path", + "required": true, + "description": "Attribute Name", + "schema": { + "type": "string" + } + }, + { + "name": "attrName", + "in": "path", + "required": true, + "description": "Attribute Name", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Business Attribute Deleted" + }, + "404": { + "description": "See #/components/responses/error" + }, + "default": { + "description": "See #/components/responses/error" + } + }, + "scopes": ["commerce.business:update"] + } + ] +} diff --git a/src/cli/schemas/api/catalog-products.json b/src/cli/schemas/api/catalog-products.json new file mode 100644 index 0000000..0f01edc --- /dev/null +++ b/src/cli/schemas/api/catalog-products.json @@ -0,0 +1,3392 @@ +{ + "name": "catalog-products", + "title": "Catalog GraphQL API", + "description": "", + "version": "2.0.0", + "baseUrl": "https://api.godaddy.com/v2/commerce", + "endpoints": [ + { + "operationId": "postCatalogGraphql", + "method": "POST", + "path": "/stores/{storeId}/catalog-subgraph", + "summary": "Catalog GraphQL endpoint", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "x-store-id", + "in": "header", + "required": true, + "description": "Store ID header required by catalog subgraph", + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "required": true, + "contentType": "application/json", + "schema": { + "type": "object", + "required": ["query"], + "properties": { + "query": { + "type": "string" + }, + "variables": { + "type": "object", + "additionalProperties": true + }, + "operationName": { + "type": "string" + } + } + } + }, + "responses": { + "200": { + "description": "GraphQL response", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "nullable": true, + "additionalProperties": true + }, + "errors": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "400": { + "description": "Invalid request" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Store not found" + }, + "500": { + "description": "Internal server error" + } + }, + "scopes": ["commerce.product:read", "commerce.product:write"], + "graphql": { + "schemaRef": "./graphql/schema.graphql", + "operationCount": 145, + "operations": [ + { + "name": "attribute", + "kind": "query", + "returnType": "Attribute", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + } + ] + }, + { + "name": "attributes", + "kind": "query", + "returnType": "QueryAttributesConnection", + "deprecated": false, + "args": [ + { + "name": "after", + "type": "String", + "required": false + }, + { + "name": "before", + "type": "String", + "required": false + }, + { + "name": "createdAt", + "type": "CreatedAtFilter", + "required": false + }, + { + "name": "first", + "type": "Int", + "required": false + }, + { + "name": "id", + "type": "AttributeIdsFilter", + "required": false + }, + { + "name": "last", + "type": "Int", + "required": false + }, + { + "name": "name", + "type": "NameFilter", + "required": false + }, + { + "name": "orderBy", + "type": "AttributeOrderBy", + "required": false + }, + { + "name": "paginationType", + "type": "PaginationType", + "required": false, + "defaultValue": "CURSOR" + }, + { + "name": "skuGroupId", + "type": "SKUGroupIdFilter", + "required": false + }, + { + "name": "updatedAt", + "type": "UpdatedAtFilter", + "required": false + } + ] + }, + { + "name": "attributeValue", + "kind": "query", + "returnType": "AttributeValue", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + } + ] + }, + { + "name": "attributeValues", + "kind": "query", + "returnType": "QueryAttributeValuesConnection", + "deprecated": false, + "args": [ + { + "name": "after", + "type": "String", + "required": false + }, + { + "name": "attributeId", + "type": "AttributeIdFilter", + "required": false + }, + { + "name": "before", + "type": "String", + "required": false + }, + { + "name": "createdAt", + "type": "CreatedAtFilter", + "required": false + }, + { + "name": "first", + "type": "Int", + "required": false + }, + { + "name": "id", + "type": "AttributeValueIdsFilter", + "required": false + }, + { + "name": "last", + "type": "Int", + "required": false + }, + { + "name": "name", + "type": "NameFilter", + "required": false + }, + { + "name": "orderBy", + "type": "AttributeValueOrderBy", + "required": false + }, + { + "name": "paginationType", + "type": "PaginationType", + "required": false, + "defaultValue": "CURSOR" + }, + { + "name": "skuId", + "type": "SKUIdFilter", + "required": false + }, + { + "name": "updatedAt", + "type": "UpdatedAtFilter", + "required": false + } + ] + }, + { + "name": "inventoryAdjustment", + "kind": "query", + "returnType": "InventoryAdjustment", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + } + ] + }, + { + "name": "inventoryAdjustments", + "kind": "query", + "returnType": "QueryInventoryAdjustmentsConnection", + "deprecated": false, + "args": [ + { + "name": "after", + "type": "String", + "required": false + }, + { + "name": "before", + "type": "String", + "required": false + }, + { + "name": "createdAt", + "type": "CreatedAtFilter", + "required": false + }, + { + "name": "first", + "type": "Int", + "required": false + }, + { + "name": "id", + "type": "InventoryAdjustmentIdsFilter", + "required": false + }, + { + "name": "last", + "type": "Int", + "required": false + }, + { + "name": "locationId", + "type": "LocationIdFilter", + "required": false + }, + { + "name": "orderBy", + "type": "InventoryAdjustmentOrderBy", + "required": false + }, + { + "name": "paginationType", + "type": "PaginationType", + "required": false, + "defaultValue": "CURSOR" + }, + { + "name": "referenceValue", + "type": "ReferenceValueFilter", + "required": false + }, + { + "name": "skuId", + "type": "SKUIdFilter", + "required": false + }, + { + "name": "type", + "type": "InventoryAdjustmentTypeFilter", + "required": false + } + ] + }, + { + "name": "inventoryCount", + "kind": "query", + "returnType": "[InventoryCount!]", + "deprecated": false, + "args": [ + { + "name": "locationId", + "type": "String!", + "required": true + }, + { + "name": "skuId", + "type": "String!", + "required": true + }, + { + "name": "type", + "type": "String", + "required": false + } + ] + }, + { + "name": "inventoryCounts", + "kind": "query", + "returnType": "QueryInventoryCountsConnection", + "deprecated": false, + "args": [ + { + "name": "after", + "type": "String", + "required": false + }, + { + "name": "before", + "type": "String", + "required": false + }, + { + "name": "createdAt", + "type": "CreatedAtFilter", + "required": false + }, + { + "name": "first", + "type": "Int", + "required": false + }, + { + "name": "id", + "type": "InventoryCountIdsFilter", + "required": false + }, + { + "name": "last", + "type": "Int", + "required": false + }, + { + "name": "locationId", + "type": "LocationIdFilter", + "required": false + }, + { + "name": "orderBy", + "type": "InventoryCountOrderBy", + "required": false + }, + { + "name": "paginationType", + "type": "PaginationType", + "required": false, + "defaultValue": "CURSOR" + }, + { + "name": "skuId", + "type": "SKUIdFilter", + "required": false + }, + { + "name": "type", + "type": "InventoryCountTypeFilter", + "required": false + }, + { + "name": "updatedAt", + "type": "UpdatedAtFilter", + "required": false + } + ] + }, + { + "name": "list", + "kind": "query", + "returnType": "List", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + } + ] + }, + { + "name": "lists", + "kind": "query", + "returnType": "QueryListsConnection", + "deprecated": false, + "args": [ + { + "name": "after", + "type": "String", + "required": false + }, + { + "name": "before", + "type": "String", + "required": false + }, + { + "name": "createdAt", + "type": "CreatedAtFilter", + "required": false + }, + { + "name": "first", + "type": "Int", + "required": false + }, + { + "name": "id", + "type": "ListIdsFilter", + "required": false + }, + { + "name": "last", + "type": "Int", + "required": false + }, + { + "name": "name", + "type": "NameFilter", + "required": false + }, + { + "name": "orderBy", + "type": "ListOrderBy", + "required": false + }, + { + "name": "paginationType", + "type": "PaginationType", + "required": false, + "defaultValue": "CURSOR" + }, + { + "name": "referenceValue", + "type": "ReferenceValueFilter", + "required": false + }, + { + "name": "skuGroupId", + "type": "SKUGroupIdFilter", + "required": false + }, + { + "name": "status", + "type": "ListStatusFilter", + "required": false + }, + { + "name": "updatedAt", + "type": "UpdatedAtFilter", + "required": false + } + ] + }, + { + "name": "listTree", + "kind": "query", + "returnType": "ListTree", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + } + ] + }, + { + "name": "listTreeNode", + "kind": "query", + "returnType": "ListTreeNode", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + } + ] + }, + { + "name": "listTreeNodes", + "kind": "query", + "returnType": "QueryListTreeNodesConnection", + "deprecated": false, + "args": [ + { + "name": "after", + "type": "String", + "required": false + }, + { + "name": "before", + "type": "String", + "required": false + }, + { + "name": "createdAt", + "type": "CreatedAtFilter", + "required": false + }, + { + "name": "first", + "type": "Int", + "required": false + }, + { + "name": "id", + "type": "ListTreeNodeIdsFilter", + "required": false + }, + { + "name": "last", + "type": "Int", + "required": false + }, + { + "name": "listId", + "type": "ListIdFilter", + "required": false + }, + { + "name": "listTreeId", + "type": "ListTreeIdFilter", + "required": false + }, + { + "name": "orderBy", + "type": "ListTreeNodeOrderBy", + "required": false + }, + { + "name": "paginationType", + "type": "PaginationType", + "required": false, + "defaultValue": "CURSOR" + }, + { + "name": "parentNodeId", + "type": "ListTreeNodeParentNodeIdFilter", + "required": false + }, + { + "name": "referenceValue", + "type": "ReferenceValueFilter", + "required": false + }, + { + "name": "updatedAt", + "type": "UpdatedAtFilter", + "required": false + } + ] + }, + { + "name": "listTrees", + "kind": "query", + "returnType": "QueryListTreesConnection", + "deprecated": false, + "args": [ + { + "name": "after", + "type": "String", + "required": false + }, + { + "name": "before", + "type": "String", + "required": false + }, + { + "name": "createdAt", + "type": "CreatedAtFilter", + "required": false + }, + { + "name": "first", + "type": "Int", + "required": false + }, + { + "name": "id", + "type": "ListTreeIdsFilter", + "required": false + }, + { + "name": "last", + "type": "Int", + "required": false + }, + { + "name": "name", + "type": "NameFilter", + "required": false + }, + { + "name": "orderBy", + "type": "ListTreeOrderBy", + "required": false + }, + { + "name": "paginationType", + "type": "PaginationType", + "required": false, + "defaultValue": "CURSOR" + }, + { + "name": "referenceValue", + "type": "ReferenceValueFilter", + "required": false + }, + { + "name": "status", + "type": "ListTreeStatusFilter", + "required": false + }, + { + "name": "updatedAt", + "type": "UpdatedAtFilter", + "required": false + } + ] + }, + { + "name": "location", + "kind": "query", + "returnType": "Location", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + } + ] + }, + { + "name": "locations", + "kind": "query", + "returnType": "QueryLocationsConnection", + "deprecated": false, + "args": [ + { + "name": "after", + "type": "String", + "required": false + }, + { + "name": "before", + "type": "String", + "required": false + }, + { + "name": "createdAt", + "type": "CreatedAtFilter", + "required": false + }, + { + "name": "first", + "type": "Int", + "required": false + }, + { + "name": "id", + "type": "LocationIdsFilter", + "required": false + }, + { + "name": "last", + "type": "Int", + "required": false + }, + { + "name": "name", + "type": "NameFilter", + "required": false + }, + { + "name": "orderBy", + "type": "LocationOrderBy", + "required": false + }, + { + "name": "paginationType", + "type": "PaginationType", + "required": false, + "defaultValue": "CURSOR" + }, + { + "name": "referenceValue", + "type": "ReferenceValueFilter", + "required": false + }, + { + "name": "skuId", + "type": "SKUIdFilter", + "required": false + }, + { + "name": "status", + "type": "LocationStatusFilter", + "required": false + }, + { + "name": "updatedAt", + "type": "UpdatedAtFilter", + "required": false + } + ] + }, + { + "name": "options", + "kind": "query", + "returnType": "QueryOptionsConnection", + "deprecated": false, + "args": [ + { + "name": "after", + "type": "String", + "required": false + }, + { + "name": "before", + "type": "String", + "required": false + }, + { + "name": "createdAt", + "type": "CreatedAtFilter", + "required": false + }, + { + "name": "first", + "type": "Int", + "required": false + }, + { + "name": "id", + "type": "OptionIdsFilter", + "required": false + }, + { + "name": "last", + "type": "Int", + "required": false + }, + { + "name": "name", + "type": "NameFilter", + "required": false + }, + { + "name": "orderBy", + "type": "OptionOrderBy", + "required": false + }, + { + "name": "paginationType", + "type": "PaginationType", + "required": false, + "defaultValue": "CURSOR" + }, + { + "name": "skuGroupId", + "type": "SKUGroupIdFilter", + "required": false + }, + { + "name": "updatedAt", + "type": "UpdatedAtFilter", + "required": false + } + ] + }, + { + "name": "sku", + "kind": "query", + "returnType": "SKU", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + } + ] + }, + { + "name": "SKU", + "kind": "query", + "returnType": "SKU", + "deprecated": true, + "deprecationReason": "Use sku instead.", + "args": [ + { + "name": "id", + "type": "String!", + "required": true + } + ] + }, + { + "name": "skuGroup", + "kind": "query", + "returnType": "SKUGroup", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + } + ] + }, + { + "name": "SKUGroup", + "kind": "query", + "returnType": "SKUGroup", + "deprecated": true, + "deprecationReason": "Use skuGroup instead.", + "args": [ + { + "name": "id", + "type": "String!", + "required": true + } + ] + }, + { + "name": "skuGroups", + "kind": "query", + "returnType": "QuerySkuGroupsConnection", + "deprecated": false, + "args": [ + { + "name": "after", + "type": "String", + "required": false + }, + { + "name": "before", + "type": "String", + "required": false + }, + { + "name": "channelId", + "type": "SKUGroupChannelIdFilter", + "required": false + }, + { + "name": "createdAt", + "type": "CreatedAtFilter", + "required": false + }, + { + "name": "first", + "type": "Int", + "required": false + }, + { + "name": "id", + "type": "SKUGroupIdsFilter", + "required": false + }, + { + "name": "label", + "type": "LabelFilter", + "required": false + }, + { + "name": "last", + "type": "Int", + "required": false + }, + { + "name": "listId", + "type": "ListIdFilter", + "required": false + }, + { + "name": "name", + "type": "NameFilter", + "required": false + }, + { + "name": "orderBy", + "type": "SKUGroupOrderBy", + "required": false + }, + { + "name": "paginationType", + "type": "PaginationType", + "required": false, + "defaultValue": "CURSOR" + }, + { + "name": "referenceValue", + "type": "ReferenceValueFilter", + "required": false + }, + { + "name": "status", + "type": "SKUGroupStatusFilter", + "required": false + }, + { + "name": "updatedAt", + "type": "UpdatedAtFilter", + "required": false + } + ] + }, + { + "name": "SKUGroups", + "kind": "query", + "returnType": "QuerySKUGroupsConnection", + "deprecated": true, + "deprecationReason": "Use skuGroups instead.", + "args": [ + { + "name": "after", + "type": "String", + "required": false + }, + { + "name": "before", + "type": "String", + "required": false + }, + { + "name": "channelId", + "type": "SKUGroupChannelIdFilter", + "required": false + }, + { + "name": "createdAt", + "type": "CreatedAtFilter", + "required": false + }, + { + "name": "first", + "type": "Int", + "required": false + }, + { + "name": "id", + "type": "SKUGroupIdsFilter", + "required": false + }, + { + "name": "last", + "type": "Int", + "required": false + }, + { + "name": "listId", + "type": "ListIdFilter", + "required": false + }, + { + "name": "name", + "type": "NameFilter", + "required": false + }, + { + "name": "orderBy", + "type": "SKUGroupOrderBy", + "required": false + }, + { + "name": "paginationType", + "type": "PaginationType", + "required": false, + "defaultValue": "CURSOR" + }, + { + "name": "referenceValue", + "type": "ReferenceValueFilter", + "required": false + }, + { + "name": "status", + "type": "SKUGroupStatusFilter", + "required": false + }, + { + "name": "updatedAt", + "type": "UpdatedAtFilter", + "required": false + } + ] + }, + { + "name": "skus", + "kind": "query", + "returnType": "QuerySkusConnection", + "deprecated": false, + "args": [ + { + "name": "after", + "type": "String", + "required": false + }, + { + "name": "attributeValues", + "type": "SKUAttributeValueFilter", + "required": false + }, + { + "name": "before", + "type": "String", + "required": false + }, + { + "name": "code", + "type": "SKUCodeFilter", + "required": false + }, + { + "name": "createdAt", + "type": "CreatedAtFilter", + "required": false + }, + { + "name": "first", + "type": "Int", + "required": false + }, + { + "name": "id", + "type": "SKUIdsFilter", + "required": false + }, + { + "name": "label", + "type": "LabelFilter", + "required": false + }, + { + "name": "last", + "type": "Int", + "required": false + }, + { + "name": "locationId", + "type": "LocationIdFilter", + "required": false + }, + { + "name": "name", + "type": "NameFilter", + "required": false + }, + { + "name": "orderBy", + "type": "SKUOrderBy", + "required": false + }, + { + "name": "paginationType", + "type": "PaginationType", + "required": false, + "defaultValue": "CURSOR" + }, + { + "name": "queryFilter", + "type": "String", + "required": false, + "description": "A string filter in the ODATA format. ex: \"label eq 'shirt' and contains(code, '123')\".\n\n\t\nSupported columns: `label`, `code`\n\t\nSupported operators: `eq`, `ne`, `lt`, `gt`, `and`, `or`\n\t\nSupported functions: `contains`" + }, + { + "name": "referenceValue", + "type": "ReferenceValueFilter", + "required": false + }, + { + "name": "skuGroupId", + "type": "SKUGroupIdFilter", + "required": false + }, + { + "name": "status", + "type": "SKUStatusFilter", + "required": false + }, + { + "name": "updatedAt", + "type": "UpdatedAtFilter", + "required": false + } + ] + }, + { + "name": "SKUs", + "kind": "query", + "returnType": "QuerySKUsConnection", + "deprecated": true, + "deprecationReason": "Use skus instead.", + "args": [ + { + "name": "after", + "type": "String", + "required": false + }, + { + "name": "attributeValues", + "type": "SKUAttributeValueFilter", + "required": false + }, + { + "name": "before", + "type": "String", + "required": false + }, + { + "name": "createdAt", + "type": "CreatedAtFilter", + "required": false + }, + { + "name": "first", + "type": "Int", + "required": false + }, + { + "name": "id", + "type": "SKUIdsFilter", + "required": false + }, + { + "name": "last", + "type": "Int", + "required": false + }, + { + "name": "locationId", + "type": "LocationIdFilter", + "required": false + }, + { + "name": "name", + "type": "NameFilter", + "required": false + }, + { + "name": "orderBy", + "type": "SKUOrderBy", + "required": false + }, + { + "name": "paginationType", + "type": "PaginationType", + "required": false, + "defaultValue": "CURSOR" + }, + { + "name": "referenceValue", + "type": "ReferenceValueFilter", + "required": false + }, + { + "name": "skuGroupId", + "type": "SKUGroupIdFilter", + "required": false + }, + { + "name": "status", + "type": "SKUStatusFilter", + "required": false + }, + { + "name": "updatedAt", + "type": "UpdatedAtFilter", + "required": false + } + ] + }, + { + "name": "addAttributesToSkuGroup", + "kind": "mutation", + "returnType": "SKUGroup", + "description": "Add Attributes to an existing SKU group.", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationAddAttributesToSkuGroupInput!", + "required": true + } + ] + }, + { + "name": "addAttributesToSKUGroup", + "kind": "mutation", + "returnType": "SKUGroup", + "description": "Add Attributes to an existing SKU group.", + "deprecated": true, + "deprecationReason": "Use `addAttributesToSkuGroup` instead.", + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationAddAttributesToSKUGroupInput!", + "required": true + } + ] + }, + { + "name": "addAttributeValuesToSku", + "kind": "mutation", + "returnType": "SKU", + "description": "Add Attribute Value(s) to an existing SKU.", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationAddAttributeValuesToSkuInput!", + "required": true + } + ] + }, + { + "name": "addAttributeValuesToSKU", + "kind": "mutation", + "returnType": "SKU", + "description": "Add Attribute Value(s) to an existing SKU.", + "deprecated": true, + "deprecationReason": "Use `addAttributeValuesToSku` instead.", + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationAddAttributeValuesToSKUInput!", + "required": true + } + ] + }, + { + "name": "addChannelsToSkuGroup", + "kind": "mutation", + "returnType": "SKUGroup", + "description": "Add channels to an existing SKU group.", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationAddChannelsToSkuGroupInput!", + "required": true + } + ] + }, + { + "name": "addChannelsToSKUGroup", + "kind": "mutation", + "returnType": "SKUGroup", + "description": "Add channels to an existing SKU group.", + "deprecated": true, + "deprecationReason": "Use `addChannelsToSkuGroup` instead.", + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationAddChannelsToSKUGroupInput!", + "required": true + } + ] + }, + { + "name": "addListTreeNodesToListTree", + "kind": "mutation", + "returnType": "ListTree", + "description": "Add a new node to a list tree.", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationAddListTreeNodesToListTreeInput!", + "required": true + } + ] + }, + { + "name": "addListTreeNodesToParentListTreeNode", + "kind": "mutation", + "returnType": "ListTreeNode", + "description": "Add a new node to a list tree node.", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationAddListTreeNodesToParentListTreeNodeInput!", + "required": true + } + ] + }, + { + "name": "addMediaObjectsToList", + "kind": "mutation", + "returnType": "List", + "description": "Add media objects to an existing List.", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationAddMediaObjectsToListInput!", + "required": true + } + ] + }, + { + "name": "addMediaObjectsToSku", + "kind": "mutation", + "returnType": "SKU", + "description": "Add media objects to an existing SKU", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationAddMediaObjectsToSkuInput!", + "required": true + } + ] + }, + { + "name": "addMediaObjectsToSKU", + "kind": "mutation", + "returnType": "SKU", + "description": "Add media objects to an existing SKU", + "deprecated": true, + "deprecationReason": "Use `addMediaObjectsToSku` instead.", + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationAddMediaObjectsToSKUInput!", + "required": true + } + ] + }, + { + "name": "addMediaObjectsToSkuGroup", + "kind": "mutation", + "returnType": "SKUGroup", + "description": "Add media objects to an existing SKU group.", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationAddMediaObjectsToSkuGroupInput!", + "required": true + } + ] + }, + { + "name": "addMediaObjectsToSKUGroup", + "kind": "mutation", + "returnType": "SKUGroup", + "description": "Add media objects to an existing SKU group.", + "deprecated": true, + "deprecationReason": "Use `addMediaObjectsToSkuGroup` instead.", + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationAddMediaObjectsToSKUGroupInput!", + "required": true + } + ] + }, + { + "name": "addOptionsToSkuGroup", + "kind": "mutation", + "returnType": "SKUGroup", + "description": "Add options to an existing SKU group.", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationAddOptionsToSkuGroupInput!", + "required": true + } + ] + }, + { + "name": "addOptionsToSKUGroup", + "kind": "mutation", + "returnType": "SKUGroup", + "description": "Add options to an existing SKU group.", + "deprecated": true, + "deprecationReason": "Use `addOptionsToSkuGroup` instead.", + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationAddOptionsToSKUGroupInput!", + "required": true + } + ] + }, + { + "name": "addReferencesToList", + "kind": "mutation", + "returnType": "List", + "description": "Add references to an existing List.", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationAddReferencesToListInput!", + "required": true + } + ] + }, + { + "name": "addReferencesToListTree", + "kind": "mutation", + "returnType": "ListTree", + "description": "Add references to an existing List Tree.", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationAddReferencesToListTreeInput!", + "required": true + } + ] + }, + { + "name": "addReferencesToListTreeNode", + "kind": "mutation", + "returnType": "ListTreeNode", + "description": "Add references to an existing List Tree Node", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationAddReferencesToListTreeNodeInput!", + "required": true + } + ] + }, + { + "name": "addReferencesToLocation", + "kind": "mutation", + "returnType": "Location", + "description": "Add references to an existing location.", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationAddReferencesToLocationInput!", + "required": true + } + ] + }, + { + "name": "addReferencesToSku", + "kind": "mutation", + "returnType": "SKU", + "description": "Add references to an existing SKU.", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationAddReferencesToSkuInput!", + "required": true + } + ] + }, + { + "name": "addReferencesToSKU", + "kind": "mutation", + "returnType": "SKU", + "description": "Add references to an existing SKU.", + "deprecated": true, + "deprecationReason": "Use `addReferencesToSku` instead.", + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationAddReferencesToSKUInput!", + "required": true + } + ] + }, + { + "name": "addReferencesToSkuGroup", + "kind": "mutation", + "returnType": "SKUGroup", + "description": "Add references to an existing SKU group.", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationAddReferencesToSkuGroupInput!", + "required": true + } + ] + }, + { + "name": "addReferencesToSKUGroup", + "kind": "mutation", + "returnType": "SKUGroup", + "description": "Add references to an existing SKU group.", + "deprecated": true, + "deprecationReason": "Use `addReferencesToSkuGroup` instead.", + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationAddReferencesToSKUGroupInput!", + "required": true + } + ] + }, + { + "name": "addSkuGroupsToList", + "kind": "mutation", + "returnType": "List", + "description": "Add SKU Groups to an existing list.", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationAddSkuGroupsToListInput!", + "required": true + } + ] + }, + { + "name": "addSKUGroupsToList", + "kind": "mutation", + "returnType": "List", + "description": "Add SKU Groups to an existing list.", + "deprecated": true, + "deprecationReason": "Use `addSkuGroupsToList` instead.", + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationAddSKUGroupsToListInput!", + "required": true + } + ] + }, + { + "name": "addSkuGroupToLists", + "kind": "mutation", + "returnType": "SKUGroup", + "description": "Add a single SKU Group to multiple lists", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationAddSkuGroupToListsInput!", + "required": true + } + ] + }, + { + "name": "addSKUGroupToLists", + "kind": "mutation", + "returnType": "SKUGroup", + "description": "Add a single SKU Group to multiple lists", + "deprecated": true, + "deprecationReason": "Use `addSkuGroupToLists` instead.", + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationAddSKUGroupToListsInput!", + "required": true + } + ] + }, + { + "name": "addSkusToLocation", + "kind": "mutation", + "returnType": "Location", + "description": "Add SKUs to an existing location.", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationAddSkusToLocationInput!", + "required": true + } + ] + }, + { + "name": "addSKUsToLocation", + "kind": "mutation", + "returnType": "Location", + "description": "Add SKUs to an existing location.", + "deprecated": true, + "deprecationReason": "Use `addSkusToLocation` instead.", + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationAddSKUsToLocationInput!", + "required": true + } + ] + }, + { + "name": "addSkusToSkuGroup", + "kind": "mutation", + "returnType": "SKUGroup", + "description": "Add SKUs to an existing SKU group.", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationAddSkusToSkuGroupInput!", + "required": true + } + ] + }, + { + "name": "addSKUsToSKUGroup", + "kind": "mutation", + "returnType": "SKUGroup", + "description": "Add SKUs to an existing SKU group.", + "deprecated": true, + "deprecationReason": "Use `addSkusToSkuGroup` instead.", + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationAddSKUsToSKUGroupInput!", + "required": true + } + ] + }, + { + "name": "archiveList", + "kind": "mutation", + "returnType": "List", + "description": "Archive a list.", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + } + ] + }, + { + "name": "archiveListTree", + "kind": "mutation", + "returnType": "ListTree", + "description": "Archive a list tree.", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + } + ] + }, + { + "name": "archiveLocation", + "kind": "mutation", + "returnType": "Location", + "description": "Archive a location.", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + } + ] + }, + { + "name": "archiveSku", + "kind": "mutation", + "returnType": "SKU", + "description": "Archive an existing SKU.", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + } + ] + }, + { + "name": "archiveSKU", + "kind": "mutation", + "returnType": "SKU", + "description": "Archive an existing SKU.", + "deprecated": true, + "deprecationReason": "Use `archiveSku` instead.", + "args": [ + { + "name": "id", + "type": "String!", + "required": true + } + ] + }, + { + "name": "archiveSkuGroup", + "kind": "mutation", + "returnType": "SKUGroup", + "description": "Archive an existing SKU Group.", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + } + ] + }, + { + "name": "archiveSKUGroup", + "kind": "mutation", + "returnType": "SKUGroup", + "description": "Archive an existing SKU Group.", + "deprecated": true, + "deprecationReason": "Use `archiveSkuGroup` instead.", + "args": [ + { + "name": "id", + "type": "String!", + "required": true + } + ] + }, + { + "name": "commitInventory", + "kind": "mutation", + "returnType": "[InventoryAdjustment!]", + "description": "Commit new inventory. If there are not enough available inventory, they will be moved to backordered if allowed. Documentation: https://godaddy-corp.atlassian.net/wiki/spaces/COMCORE/pages/3758600912/Inventory+Helper+Mutations#2.-commitInventory", + "deprecated": false, + "args": [ + { + "name": "input", + "type": "MutationCommitInventoryInput!", + "required": true + } + ] + }, + { + "name": "createAttribute", + "kind": "mutation", + "returnType": "Attribute", + "description": "Create a new attribute.", + "deprecated": false, + "args": [ + { + "name": "input", + "type": "MutationCreateAttributeInput!", + "required": true + } + ] + }, + { + "name": "createAttributeValue", + "kind": "mutation", + "returnType": "AttributeValue", + "description": "Create a new attribute value.", + "deprecated": false, + "args": [ + { + "name": "input", + "type": "CreateAttributeValueInput!", + "required": true + } + ] + }, + { + "name": "createAttributeValues", + "kind": "mutation", + "returnType": "[AttributeValue!]", + "description": "Create new attribute values.", + "deprecated": false, + "args": [ + { + "name": "input", + "type": "[CreateAttributeValueInput!]!", + "required": true + } + ] + }, + { + "name": "createInventoryAdjustment", + "kind": "mutation", + "returnType": "InventoryAdjustment", + "description": "Create a new inventory adjustment.", + "deprecated": false, + "args": [ + { + "name": "input", + "type": "MutationCreateInventoryAdjustmentInput!", + "required": true + } + ] + }, + { + "name": "createList", + "kind": "mutation", + "returnType": "List", + "description": "Create a new list.", + "deprecated": false, + "args": [ + { + "name": "input", + "type": "MutationCreateListInput!", + "required": true + } + ] + }, + { + "name": "createListTree", + "kind": "mutation", + "returnType": "ListTree", + "description": "Create a new list tree.", + "deprecated": false, + "args": [ + { + "name": "input", + "type": "MutationCreateListTreeInput!", + "required": true + } + ] + }, + { + "name": "createLocation", + "kind": "mutation", + "returnType": "Location", + "description": "Create a new location.", + "deprecated": false, + "args": [ + { + "name": "input", + "type": "MutationCreateLocationInput!", + "required": true + } + ] + }, + { + "name": "createOption", + "kind": "mutation", + "returnType": "Option", + "description": "Create a new option.", + "deprecated": false, + "args": [ + { + "name": "input", + "type": "MutationCreateOptionInput!", + "required": true + } + ] + }, + { + "name": "createOptionPrice", + "kind": "mutation", + "returnType": "OptionPrice", + "description": "Create a flat price for the option. This will be ignore if a price is present on the option values.", + "deprecated": false, + "args": [ + { + "name": "input", + "type": "MutationCreateOptionPriceInput!", + "required": true + } + ] + }, + { + "name": "createOptionValue", + "kind": "mutation", + "returnType": "OptionValue", + "description": "Create a new option value.", + "deprecated": false, + "args": [ + { + "name": "input", + "type": "MutationCreateOptionValueInput!", + "required": true + } + ] + }, + { + "name": "createOptionValuePrice", + "kind": "mutation", + "returnType": "OptionValuePrice", + "description": "Create a price.", + "deprecated": false, + "args": [ + { + "name": "input", + "type": "MutationCreateOptionValuePriceInput!", + "required": true + } + ] + }, + { + "name": "createOptionValues", + "kind": "mutation", + "returnType": "[OptionValue!]", + "description": "Create multiple new Option Values in a single transaction.", + "deprecated": false, + "args": [ + { + "name": "input", + "type": "[MutationCreateOptionValueInput!]!", + "required": true + } + ] + }, + { + "name": "createSku", + "kind": "mutation", + "returnType": "SKU", + "description": "Create a new SKU.", + "deprecated": false, + "args": [ + { + "name": "input", + "type": "CreateSKUInput!", + "required": true + } + ] + }, + { + "name": "createSKU", + "kind": "mutation", + "returnType": "SKU", + "description": "Create a new SKU.", + "deprecated": true, + "deprecationReason": "Use `createSku` instead.", + "args": [ + { + "name": "input", + "type": "CreateSKUInput!", + "required": true + } + ] + }, + { + "name": "createSkuGroup", + "kind": "mutation", + "returnType": "SKUGroup", + "description": "Create a new SKU Group.", + "deprecated": false, + "args": [ + { + "name": "input", + "type": "MutationCreateSkuGroupInput!", + "required": true + } + ] + }, + { + "name": "createSKUGroup", + "kind": "mutation", + "returnType": "SKUGroup", + "description": "Create a new SKU Group.", + "deprecated": true, + "deprecationReason": "Use `createSkuGroup` instead.", + "args": [ + { + "name": "input", + "type": "MutationCreateSKUGroupInput!", + "required": true + } + ] + }, + { + "name": "createSkuPrice", + "kind": "mutation", + "returnType": "SKUPrice", + "description": "Create a price.", + "deprecated": false, + "args": [ + { + "name": "input", + "type": "MutationCreateSkuPriceInput!", + "required": true + } + ] + }, + { + "name": "createSKUPrice", + "kind": "mutation", + "returnType": "SKUPrice", + "description": "Create a price.", + "deprecated": true, + "deprecationReason": "Use `createSkuPrice` instead.", + "args": [ + { + "name": "input", + "type": "MutationCreateSKUPriceInput!", + "required": true + } + ] + }, + { + "name": "createSkus", + "kind": "mutation", + "returnType": "[SKU!]", + "description": "Create multiple new SKUs in a single transaction.", + "deprecated": false, + "args": [ + { + "name": "input", + "type": "[CreateSKUInput!]!", + "required": true + } + ] + }, + { + "name": "createSKUs", + "kind": "mutation", + "returnType": "[SKU!]", + "description": "Create multiple new SKUs in a single transaction.", + "deprecated": true, + "deprecationReason": "Use `createSkus` instead.", + "args": [ + { + "name": "input", + "type": "[CreateSKUInput!]!", + "required": true + } + ] + }, + { + "name": "deleteSkuGroupById", + "kind": "mutation", + "returnType": "SKUGroup", + "description": "WARNING: This mutation hard deletes an existing SKU Group and all its associated records.", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + } + ] + }, + { + "name": "releaseInventory", + "kind": "mutation", + "returnType": "[InventoryAdjustment!]", + "description": "Release inventory from committed. If there are not enough committed inventory, this mutation will error. Documentation: https://godaddy-corp.atlassian.net/wiki/spaces/COMCORE/pages/3758600912/Inventory+Helper+Mutations#3.-releaseInventory", + "deprecated": false, + "args": [ + { + "name": "input", + "type": "MutationReleaseInventoryInput!", + "required": true + } + ] + }, + { + "name": "removeAttributesFromSkuGroup", + "kind": "mutation", + "returnType": "SKUGroup", + "description": "Remove Attributes from an existing SKU group.", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationRemoveAttributesFromSkuGroupInput!", + "required": true + } + ] + }, + { + "name": "removeAttributesFromSKUGroup", + "kind": "mutation", + "returnType": "SKUGroup", + "description": "Remove Attributes from an existing SKU group.", + "deprecated": true, + "deprecationReason": "Use `removeAttributesFromSkuGroup` instead.", + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationRemoveAttributesFromSKUGroupInput!", + "required": true + } + ] + }, + { + "name": "removeAttributeValuesFromAttribute", + "kind": "mutation", + "returnType": "Attribute", + "description": "Remove Attribute Value(s) from an existing Attribute.", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationRemoveAttributeValuesFromAttributeInput!", + "required": true + } + ] + }, + { + "name": "removeAttributeValuesFromSku", + "kind": "mutation", + "returnType": "SKU", + "description": "Remove Attribute Value(s) from an existing SKU.", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationRemoveAttributeValuesFromSkuInput!", + "required": true + } + ] + }, + { + "name": "removeAttributeValuesFromSKU", + "kind": "mutation", + "returnType": "SKU", + "description": "Remove Attribute Value(s) from an existing SKU.", + "deprecated": true, + "deprecationReason": "Use `removeAttributeValuesFromSku` instead.", + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationRemoveAttributeValuesFromSKUInput!", + "required": true + } + ] + }, + { + "name": "removeChannelsFromSkuGroup", + "kind": "mutation", + "returnType": "SKUGroup", + "description": "Remove channel associations from an existing SKU group.", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationRemoveChannelsFromSkuGroupInput!", + "required": true + } + ] + }, + { + "name": "removeChannelsFromSKUGroup", + "kind": "mutation", + "returnType": "SKUGroup", + "description": "Remove channel associations from an existing SKU group.", + "deprecated": true, + "deprecationReason": "Use `removeChannelsFromSkuGroup` instead.", + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationRemoveChannelsFromSKUGroupInput!", + "required": true + } + ] + }, + { + "name": "removeListTreeNodesFromListTree", + "kind": "mutation", + "returnType": "ListTree", + "description": "Remove nodes from a list tree.", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationRemoveListTreeNodesFromListTreeInput!", + "required": true + } + ] + }, + { + "name": "removeListTreeNodesFromParentListTreeNode", + "kind": "mutation", + "returnType": "ListTreeNode", + "description": "Remove nodes from a list tree node.", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationRemoveListTreeNodesFromParentListTreeNodeInput!", + "required": true + } + ] + }, + { + "name": "removeMediaObjectsFromList", + "kind": "mutation", + "returnType": "List", + "description": "Remove media objects from an existing List.", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationRemoveMediaObjectsFromListInput!", + "required": true + } + ] + }, + { + "name": "removeMediaObjectsFromSku", + "kind": "mutation", + "returnType": "SKU", + "description": "Remove media objects from an existing SKU", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationRemoveMediaObjectsFromSkuInput!", + "required": true + } + ] + }, + { + "name": "removeMediaObjectsFromSKU", + "kind": "mutation", + "returnType": "SKU", + "description": "Remove media objects from an existing SKU", + "deprecated": true, + "deprecationReason": "Use `removeMediaObjectsFromSku` instead.", + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationRemoveMediaObjectsFromSKUInput!", + "required": true + } + ] + }, + { + "name": "removeMediaObjectsFromSkuGroup", + "kind": "mutation", + "returnType": "SKUGroup", + "description": "Remove media objects from an existing SKU group.", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationRemoveMediaObjectsFromSkuGroupInput!", + "required": true + } + ] + }, + { + "name": "removeMediaObjectsFromSKUGroup", + "kind": "mutation", + "returnType": "SKUGroup", + "description": "Remove media objects from an existing SKU group.", + "deprecated": true, + "deprecationReason": "Use `removeMediaObjectsFromSkuGroup` instead.", + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationRemoveMediaObjectsFromSKUGroupInput!", + "required": true + } + ] + }, + { + "name": "removeOption", + "kind": "mutation", + "returnType": "Null", + "description": "Remove an existing option.", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + } + ] + }, + { + "name": "removeOptionsFromSkuGroup", + "kind": "mutation", + "returnType": "SKUGroup", + "description": "Remove Options from an existing SKU group.", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationRemoveOptionsFromSkuGroupInput!", + "required": true + } + ] + }, + { + "name": "removeOptionsFromSKUGroup", + "kind": "mutation", + "returnType": "SKUGroup", + "description": "Remove Options from an existing SKU group.", + "deprecated": true, + "deprecationReason": "Use `removeOptionsFromSkuGroup` instead.", + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationRemoveOptionsFromSKUGroupInput!", + "required": true + } + ] + }, + { + "name": "removeOptionValue", + "kind": "mutation", + "returnType": "Null", + "description": "Remove an existing option value.", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + } + ] + }, + { + "name": "removeOptionValuesFromListOption", + "kind": "mutation", + "returnType": "ListOption", + "description": "Remove values from an existing Option.", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationRemoveOptionValuesFromListOptionInput!", + "required": true + } + ] + }, + { + "name": "removePricesFromOption", + "kind": "mutation", + "returnType": "Option", + "description": "Remove prices from an existing Option.", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationRemovePricesFromOptionInput!", + "required": true + } + ] + }, + { + "name": "removePricesFromOptionValue", + "kind": "mutation", + "returnType": "OptionValue", + "description": "Remove prices from an existing OptionValue.", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationRemovePricesFromOptionValueInput!", + "required": true + } + ] + }, + { + "name": "removePricesFromSku", + "kind": "mutation", + "returnType": "SKU", + "description": "Remove prices from an existing SKU.", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationRemovePricesFromSkuInput!", + "required": true + } + ] + }, + { + "name": "removePricesFromSKU", + "kind": "mutation", + "returnType": "SKU", + "description": "Remove prices from an existing SKU.", + "deprecated": true, + "deprecationReason": "Use `removePricesFromSku` instead.", + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationRemovePricesFromSKUInput!", + "required": true + } + ] + }, + { + "name": "removeReferencesFromList", + "kind": "mutation", + "returnType": "List", + "description": "Remove references from an existing List.", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationRemoveReferencesFromListInput!", + "required": true + } + ] + }, + { + "name": "removeReferencesFromListTree", + "kind": "mutation", + "returnType": "ListTree", + "description": "Remove references from an existing List Tree", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationRemoveReferencesFromListTreeInput!", + "required": true + } + ] + }, + { + "name": "removeReferencesFromListTreeNode", + "kind": "mutation", + "returnType": "ListTreeNode", + "description": "Remove references from an existing List Tree Node", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationRemoveReferencesFromListTreeNodeInput!", + "required": true + } + ] + }, + { + "name": "removeReferencesFromLocation", + "kind": "mutation", + "returnType": "Location", + "description": "Remove references from an existing location.", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationRemoveReferencesFromLocationInput!", + "required": true + } + ] + }, + { + "name": "removeReferencesFromSku", + "kind": "mutation", + "returnType": "SKU", + "description": "Remove references from an existing SKU.", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationRemoveReferencesFromSkuInput!", + "required": true + } + ] + }, + { + "name": "removeReferencesFromSKU", + "kind": "mutation", + "returnType": "SKU", + "description": "Remove references from an existing SKU.", + "deprecated": true, + "deprecationReason": "Use `removeReferencesFromSku` instead.", + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationRemoveReferencesFromSKUInput!", + "required": true + } + ] + }, + { + "name": "removeReferencesFromSkuGroup", + "kind": "mutation", + "returnType": "SKUGroup", + "description": "Remove references from an existing SKU group.", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationRemoveReferencesFromSkuGroupInput!", + "required": true + } + ] + }, + { + "name": "removeReferencesFromSKUGroup", + "kind": "mutation", + "returnType": "SKUGroup", + "description": "Remove references from an existing SKU group.", + "deprecated": true, + "deprecationReason": "Use `removeReferencesFromSkuGroup` instead.", + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationRemoveReferencesFromSKUGroupInput!", + "required": true + } + ] + }, + { + "name": "removeSkuGroupFromLists", + "kind": "mutation", + "returnType": "SKUGroup", + "description": "Remove a single SKU Group from multiple lists", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationRemoveSkuGroupFromListsInput!", + "required": true + } + ] + }, + { + "name": "removeSKUGroupFromLists", + "kind": "mutation", + "returnType": "SKUGroup", + "description": "Remove a single SKU Group from multiple lists", + "deprecated": true, + "deprecationReason": "Use `removeSkuGroupFromLists` instead.", + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationRemoveSKUGroupFromListsInput!", + "required": true + } + ] + }, + { + "name": "removeSkuGroupsFromList", + "kind": "mutation", + "returnType": "List", + "description": "Remove SKU Groups from an existing list.", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationRemoveSkuGroupsFromListInput!", + "required": true + } + ] + }, + { + "name": "removeSKUGroupsFromList", + "kind": "mutation", + "returnType": "List", + "description": "Remove SKU Groups from an existing list.", + "deprecated": true, + "deprecationReason": "Use `removeSkuGroupsFromList` instead.", + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationRemoveSKUGroupsFromListInput!", + "required": true + } + ] + }, + { + "name": "removeSkusFromLocation", + "kind": "mutation", + "returnType": "Location", + "description": "Remove SKU from an existing location.", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationRemoveSkusFromLocationInput!", + "required": true + } + ] + }, + { + "name": "removeSKUsFromLocation", + "kind": "mutation", + "returnType": "Location", + "description": "Remove SKU from an existing location.", + "deprecated": true, + "deprecationReason": "Use `removeSkusFromLocation` instead.", + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationRemoveSKUsFromLocationInput!", + "required": true + } + ] + }, + { + "name": "removeSkusFromSkuGroup", + "kind": "mutation", + "returnType": "SKUGroup", + "description": "Remove SKUs from an existing SKU group.", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationRemoveSkusFromSkuGroupInput!", + "required": true + } + ] + }, + { + "name": "removeSKUsFromSKUGroup", + "kind": "mutation", + "returnType": "SKUGroup", + "description": "Remove SKUs from an existing SKU group.", + "deprecated": true, + "deprecationReason": "Use `removeSkusFromSkuGroup` instead.", + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationRemoveSKUsFromSKUGroupInput!", + "required": true + } + ] + }, + { + "name": "stockInventory", + "kind": "mutation", + "returnType": "[InventoryAdjustment!]", + "description": "Stock new inventory. If there are backordered inventory, they will be moved to committed. Any extra inventory will be added to available. Documentation: https://godaddy-corp.atlassian.net/wiki/spaces/COMCORE/pages/3758600912/Inventory+Helper+Mutations#1.-stockInventory", + "deprecated": false, + "args": [ + { + "name": "input", + "type": "MutationStockInventoryInput!", + "required": true + } + ] + }, + { + "name": "updateAttribute", + "kind": "mutation", + "returnType": "Attribute", + "description": "Update an existing attribute.", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationUpdateAttributeInput!", + "required": true + } + ] + }, + { + "name": "updateAttributeValue", + "kind": "mutation", + "returnType": "AttributeValue", + "description": "Update an existing attribute value.", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationUpdateAttributeValueInput!", + "required": true + } + ] + }, + { + "name": "updateList", + "kind": "mutation", + "returnType": "List", + "description": "Update an existing list.", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationUpdateListInput!", + "required": true + } + ] + }, + { + "name": "updateListMediaObjects", + "kind": "mutation", + "returnType": "List", + "description": "Add media objects or update existing media objects to existing List.", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationUpdateListMediaObjectsInput!", + "required": true + } + ] + }, + { + "name": "updateListTree", + "kind": "mutation", + "returnType": "ListTree", + "description": "Update an existing list tree.", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationUpdateListTreeInput!", + "required": true + } + ] + }, + { + "name": "updateListTreeNode", + "kind": "mutation", + "returnType": "ListTreeNode", + "description": "Update an existing list tree node.", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationUpdateListTreeNodeInput!", + "required": true + } + ] + }, + { + "name": "updateLocation", + "kind": "mutation", + "returnType": "Location", + "description": "Update an existing location.", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationUpdateLocationInput!", + "required": true + } + ] + }, + { + "name": "updateOption", + "kind": "mutation", + "returnType": "Option", + "description": "Update an existing option.", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationUpdateOptionInput!", + "required": true + } + ] + }, + { + "name": "updateOptionPrice", + "kind": "mutation", + "returnType": "OptionPrice", + "description": "Update an existing price object for an option.", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationUpdateOptionPriceInput!", + "required": true + } + ] + }, + { + "name": "updateOptionValue", + "kind": "mutation", + "returnType": "OptionValue", + "description": "Update an existing option value.", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationUpdateOptionValueInput!", + "required": true + } + ] + }, + { + "name": "updateOptionValuePrice", + "kind": "mutation", + "returnType": "OptionValuePrice", + "description": "Update an existing price object.", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationUpdateOptionValuePriceInput!", + "required": true + } + ] + }, + { + "name": "updateSku", + "kind": "mutation", + "returnType": "SKU", + "description": "Update an existing SKU.", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationUpdateSkuInput!", + "required": true + } + ] + }, + { + "name": "updateSKU", + "kind": "mutation", + "returnType": "SKU", + "description": "Update an existing SKU.", + "deprecated": true, + "deprecationReason": "Use `updateSku` instead.", + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationUpdateSKUInput!", + "required": true + } + ] + }, + { + "name": "updateSkuGroup", + "kind": "mutation", + "returnType": "SKUGroup", + "description": "Update an existing SKU Group.", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationUpdateSkuGroupInput!", + "required": true + } + ] + }, + { + "name": "updateSKUGroup", + "kind": "mutation", + "returnType": "SKUGroup", + "description": "Update an existing SKU Group.", + "deprecated": true, + "deprecationReason": "Use `updateSkuGroup` instead.", + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationUpdateSKUGroupInput!", + "required": true + } + ] + }, + { + "name": "updateSkuGroupMediaObjects", + "kind": "mutation", + "returnType": "SKUGroup", + "description": "Add media objects or update existing media objects to existing SKU Group.", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationUpdateSkuGroupMediaObjectsInput!", + "required": true + } + ] + }, + { + "name": "updateSkuMediaObjects", + "kind": "mutation", + "returnType": "SKU", + "description": "Add media objects or update existing media objects to existing SKU.", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationUpdateSkuMediaObjectsInput!", + "required": true + } + ] + }, + { + "name": "updateSkuPrice", + "kind": "mutation", + "returnType": "SKUPrice", + "description": "Update an existing price object.", + "deprecated": false, + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationUpdateSkuPriceInput!", + "required": true + } + ] + }, + { + "name": "updateSKUPrice", + "kind": "mutation", + "returnType": "SKUPrice", + "description": "Update an existing price object.", + "deprecated": true, + "deprecationReason": "Use `updateSkuPrice` instead.", + "args": [ + { + "name": "id", + "type": "String!", + "required": true + }, + { + "name": "input", + "type": "MutationUpdateSKUPriceInput!", + "required": true + } + ] + } + ] + } + } + ] +} diff --git a/src/cli/schemas/api/channels.json b/src/cli/schemas/api/channels.json new file mode 100644 index 0000000..4fa4179 --- /dev/null +++ b/src/cli/schemas/api/channels.json @@ -0,0 +1,306 @@ +{ + "name": "channels", + "title": "Channels API", + "description": "", + "version": "1.0.0", + "baseUrl": "https://api.godaddy.com/v1/commerce", + "endpoints": [ + { + "operationId": "createChannel", + "method": "POST", + "path": "/channels", + "summary": "Create a new channel", + "requestBody": { + "required": false, + "contentType": "application/json", + "schema": { + "$ref": "./models/Channel.yaml" + } + }, + "responses": { + "201": { + "description": "New resource created successfully", + "schema": { + "$ref": "./models/Channel.yaml" + } + } + }, + "scopes": ["commerce.channel:create"] + }, + { + "operationId": "getChannels", + "method": "GET", + "path": "/channels", + "summary": "Get all channels", + "description": "Retrieve all the channels for a specific provider using parameters", + "parameters": [ + { + "name": "externalChannelId", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "registeredStores.storeId", + "in": "query", + "required": false, + "schema": { + "$ref": "./types/Id.yaml" + } + }, + { + "name": "registeredStores.default", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "nullable": true + } + }, + { + "name": "pageSize", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "default": 10, + "maximum": 100 + } + }, + { + "name": "page", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "default": 1, + "maximum": 100 + } + }, + { + "name": "totalRequired", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "default": false + } + } + ], + "responses": { + "200": { + "description": "Channels", + "schema": { + "$ref": "./models/ChannelList.yaml" + } + } + }, + "scopes": ["commerce.channel:read"] + }, + { + "operationId": "patchChannel", + "method": "PATCH", + "path": "/channels/{channelId}", + "summary": "Update channel", + "description": "Update the channel information using the channel ID.", + "parameters": [ + { + "name": "channelId", + "in": "path", + "required": true, + "schema": { + "$ref": "./types/Id.yaml" + } + } + ], + "requestBody": { + "required": false, + "contentType": "application/json", + "schema": { + "items": { + "$ref": "./types/JsonPatch.yaml" + }, + "type": "array" + } + }, + "responses": { + "200": { + "description": "Channel updated", + "schema": { + "$ref": "./models/Channel.yaml" + } + }, + "404": { + "description": "Channel Not Found" + } + }, + "scopes": ["commerce.channel:update"] + }, + { + "operationId": "getChannelById", + "method": "GET", + "path": "/channels/{channelId}", + "summary": "Get channel by ID", + "description": "Retrieve the information of a single channel using the Channel ID.", + "parameters": [ + { + "name": "channelId", + "in": "path", + "required": true, + "schema": { + "$ref": "./types/Id.yaml" + } + } + ], + "responses": { + "200": { + "description": "Channel Found", + "schema": { + "$ref": "./models/Channel.yaml" + } + }, + "404": { + "description": "Channel Not Found" + } + }, + "scopes": ["commerce.channel:read"] + }, + { + "operationId": "createChannelProvider", + "method": "POST", + "path": "/channel-providers", + "summary": "Create a new channel provider", + "requestBody": { + "required": false, + "contentType": "application/json", + "schema": { + "$ref": "./models/ChannelProvider.yaml" + } + }, + "responses": { + "201": { + "description": "New resource created successfully", + "schema": { + "$ref": "./models/ChannelProvider.yaml" + } + } + }, + "scopes": ["commerce.channel-provider:create"] + }, + { + "operationId": "getChannelProviders", + "method": "GET", + "path": "/channel-providers", + "summary": "Get all channel providers", + "parameters": [ + { + "name": "pageSize", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "default": 10, + "maximum": 100 + } + }, + { + "name": "page", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "default": 1, + "maximum": 100 + } + }, + { + "name": "totalRequired", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "default": false + } + } + ], + "responses": { + "200": { + "description": "Channel Providers", + "schema": { + "$ref": "./models/ChannelProviderList.yaml" + } + } + }, + "scopes": ["commerce.channel-provider:read"] + }, + { + "operationId": "patchChannelProvider", + "method": "PATCH", + "path": "/channel-providers/{channelProviderId}", + "summary": "Update channel provider", + "parameters": [ + { + "name": "channelProviderId", + "in": "path", + "required": true, + "schema": { + "$ref": "./types/Id.yaml" + } + } + ], + "requestBody": { + "required": false, + "contentType": "application/json", + "schema": { + "items": { + "$ref": "./types/JsonPatch.yaml" + }, + "type": "array" + } + }, + "responses": { + "200": { + "description": "Channel Provider updated", + "schema": { + "$ref": "./models/ChannelProvider.yaml" + } + }, + "404": { + "description": "Channel Provider Not Found" + } + }, + "scopes": ["commerce.channel-provider:update"] + }, + { + "operationId": "getChannelProviderById", + "method": "GET", + "path": "/channel-providers/{channelProviderId}", + "summary": "Get channel provider by ID", + "description": "Retrieve the information of the channel provider using the ID.", + "parameters": [ + { + "name": "channelProviderId", + "in": "path", + "required": true, + "schema": { + "$ref": "./types/Id.yaml" + } + } + ], + "responses": { + "200": { + "description": "Channel provider Found", + "schema": { + "$ref": "./models/ChannelProvider.yaml" + } + }, + "404": { + "description": "Channel provider Not Found" + } + }, + "scopes": ["commerce.channel-provider:read"] + } + ] +} diff --git a/src/cli/schemas/api/fulfillments.json b/src/cli/schemas/api/fulfillments.json new file mode 100644 index 0000000..be7b298 --- /dev/null +++ b/src/cli/schemas/api/fulfillments.json @@ -0,0 +1,269 @@ +{ + "name": "fulfillments", + "title": "Fulfillment Service", + "description": "The Fulfillment API is intended to be a key component of the Sell Everywhere Engine, our next-generation ecommerce system that will consolidate functionality between Online Store, Sellbrite, Woo, and Poynt.", + "version": "1.1.0", + "baseUrl": "https://fulfillment.api.commerce.godaddy.com/v1/commerce", + "endpoints": [ + { + "operationId": "commerce.fulfillments.create", + "method": "POST", + "path": "/stores/{storeId}/fulfillments", + "summary": "Create a new fulfillment", + "description": "Create a new fulfillment", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "Store ID", + "schema": { + "$ref": "./models/FulfillmentPlan.yaml#/properties/storeId" + } + } + ], + "requestBody": { + "required": true, + "description": "The fulfillment object to create", + "contentType": "application/json", + "schema": { + "$ref": "./models/FulfillmentConditionals.yaml" + } + }, + "responses": { + "200": { + "description": "Successfully created the fulfillment", + "schema": { + "type": "object", + "required": ["status", "fulfillment"], + "properties": { + "status": { + "type": "string", + "description": "Status of the response", + "example": "success" + }, + "fulfillment": { + "$ref": "#/components/schemas/Fulfillment" + } + } + } + }, + "401": { + "description": "See #/components/responses/401" + }, + "403": { + "description": "See #/components/responses/403" + }, + "404": { + "description": "See #/components/responses/404" + }, + "409": { + "description": "See #/components/responses/409" + }, + "422": { + "description": "See #/components/responses/422" + }, + "500": { + "description": "See #/components/responses/500" + }, + "502": { + "description": "See #/components/responses/502" + }, + "503": { + "description": "See #/components/responses/503" + }, + "504": { + "description": "See #/components/responses/504" + } + }, + "scopes": [] + }, + { + "operationId": "commerce.fulfillments.patch", + "method": "PATCH", + "path": "/stores/{storeId}/fulfillments/{fulfillmentId}", + "summary": "Patch a fulfillment", + "description": "Modify an existing fulfillment using the fulfillment ID.", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "Store ID", + "schema": { + "$ref": "./models/FulfillmentPlan.yaml#/properties/storeId" + } + }, + { + "name": "fulfillmentId", + "in": "path", + "required": true, + "description": "The ID of the fulfillment", + "schema": { + "$ref": "./models/Fulfillment.yaml#/properties/id" + } + } + ], + "requestBody": { + "required": true, + "description": "The fulfillment object to patch", + "contentType": "application/json", + "schema": { + "$ref": "./models/Fulfillment.yaml" + } + }, + "responses": { + "200": { + "description": "Successfully patched the fulfillment", + "schema": { + "type": "object", + "required": ["status", "fulfillment"], + "properties": { + "status": { + "type": "string", + "description": "Status of the response", + "example": "success" + }, + "fulfillment": { + "$ref": "./models/Fulfillment.yaml" + } + } + } + }, + "401": { + "description": "See #/components/responses/401" + }, + "403": { + "description": "See #/components/responses/403" + }, + "404": { + "description": "See #/components/responses/404" + }, + "409": { + "description": "See #/components/responses/409" + }, + "422": { + "description": "See #/components/responses/422" + }, + "500": { + "description": "See #/components/responses/500" + }, + "502": { + "description": "See #/components/responses/502" + }, + "503": { + "description": "See #/components/responses/503" + }, + "504": { + "description": "See #/components/responses/504" + } + }, + "scopes": [] + }, + { + "operationId": "commerce.fulfillmentPlans.query", + "method": "GET", + "path": "/stores/{storeId}/fulfillmentPlans", + "summary": "Retrieve and filter fulfillments", + "description": "Retrieve all fulfillment plans filtered by the criteria provided", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "Store ID", + "schema": { + "$ref": "./models/FulfillmentPlan.yaml#/properties/storeId" + } + }, + { + "name": "orderId", + "in": "query", + "required": true, + "description": "Order ID", + "schema": { + "$ref": "./models/FulfillmentPlan.yaml#/properties/orderId" + } + }, + { + "name": "includeFulfillments", + "in": "query", + "required": false, + "description": "Determines if the response contains fulfillments associated with the fulfillment plans", + "schema": { + "type": "boolean", + "description": "Determines if fulfillments should be returned alongs fulfillment plans", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Successfully listed fulfillment plans", + "schema": { + "type": "object", + "required": ["status", "fulfillmentPlans"], + "properties": { + "status": { + "type": "string", + "description": "Status of the response", + "example": "success" + }, + "fulfillmentPlans": { + "type": "array", + "description": "List of fulfillment plans", + "items": { + "allOf": [ + { + "$ref": "#/components/schemas/FulfillmentPlan" + }, + { + "type": "object", + "properties": { + "fulfillments": { + "type": "array", + "description": "List of fulfillments associated to the fulfillment plan", + "items": { + "$ref": "#/components/schemas/Fulfillment" + } + } + } + } + ] + } + } + } + } + }, + "401": { + "description": "See #/components/responses/401" + }, + "403": { + "description": "See #/components/responses/403" + }, + "404": { + "description": "See #/components/responses/404" + }, + "409": { + "description": "See #/components/responses/409" + }, + "422": { + "description": "See #/components/responses/422" + }, + "500": { + "description": "See #/components/responses/500" + }, + "502": { + "description": "See #/components/responses/502" + }, + "503": { + "description": "See #/components/responses/503" + }, + "504": { + "description": "See #/components/responses/504" + } + }, + "scopes": [] + } + ] +} diff --git a/src/cli/schemas/api/index.ts b/src/cli/schemas/api/index.ts index 4946ab1..88c4997 100644 --- a/src/cli/schemas/api/index.ts +++ b/src/cli/schemas/api/index.ts @@ -14,8 +14,8 @@ import * as Option from "effect/Option"; // Static imports — esbuild inlines these at bundle time // --------------------------------------------------------------------------- -import locationAddressesJson from "./location-addresses.json"; import manifestJson from "./manifest.json"; +import { DOMAIN_REGISTRY } from "./registry.generated"; // --------------------------------------------------------------------------- // Types (match the generate-api-catalog output) @@ -41,6 +41,30 @@ export interface CatalogResponse { schema?: Record; } +export interface CatalogGraphqlArgument { + name: string; + type: string; + required: boolean; + description?: string; + defaultValue?: string; +} + +export interface CatalogGraphqlOperation { + name: string; + kind: "query" | "mutation"; + returnType: string; + description?: string; + deprecated: boolean; + deprecationReason?: string; + args: CatalogGraphqlArgument[]; +} + +export interface CatalogGraphqlSchema { + schemaRef: string; + operationCount: number; + operations: CatalogGraphqlOperation[]; +} + export interface CatalogEndpoint { operationId: string; method: string; @@ -51,6 +75,7 @@ export interface CatalogEndpoint { requestBody?: CatalogRequestBody; responses: Record; scopes: string[]; + graphql?: CatalogGraphqlSchema; } export interface CatalogDomain { @@ -74,13 +99,10 @@ interface CatalogManifest { } // --------------------------------------------------------------------------- -// Domain registry — maps domain names to their inlined JSON data. -// When adding a new spec, add its import above and register it here. +// Domain registry — generated at build time by generate-api-catalog.ts // --------------------------------------------------------------------------- -const DOMAIN_REGISTRY: Record = { - "location-addresses": locationAddressesJson as unknown as CatalogDomain, -}; +const domainRegistry = DOMAIN_REGISTRY as Record; const manifest = manifestJson as unknown as CatalogManifest; @@ -124,7 +146,7 @@ export function listDomainsEffect(): Effect.Effect< export function loadDomainEffect( name: string, ): Effect.Effect> { - const domain = DOMAIN_REGISTRY[name]; + const domain = domainRegistry[name]; return Effect.succeed(domain ? Option.some(domain) : Option.none()); } @@ -137,7 +159,7 @@ export function findEndpointByOperationIdEffect( Option.Option<{ domain: CatalogDomain; endpoint: CatalogEndpoint }> > { return Effect.sync(() => { - for (const domain of Object.values(DOMAIN_REGISTRY)) { + for (const domain of Object.values(domainRegistry)) { const endpoint = domain.endpoints.find( (e) => e.operationId === operationId, ); @@ -150,6 +172,50 @@ export function findEndpointByOperationIdEffect( /** * Find an endpoint by HTTP method + path across all domains. */ +function normalizeComparablePath(apiPath: string): string { + const pathOnly = apiPath.split(/[?#]/, 1)[0] || "/"; + const withLeadingSlash = pathOnly.startsWith("/") ? pathOnly : `/${pathOnly}`; + + if (withLeadingSlash.length > 1 && withLeadingSlash.endsWith("/")) { + return withLeadingSlash.slice(0, -1); + } + + return withLeadingSlash; +} + +function pathTemplateMatches( + templatePath: string, + actualPath: string, +): boolean { + const normalizedTemplate = normalizeComparablePath(templatePath); + const normalizedActual = normalizeComparablePath(actualPath); + + if (normalizedTemplate === normalizedActual) return true; + + const templateSegments = normalizedTemplate.split("/").filter(Boolean); + const actualSegments = normalizedActual.split("/").filter(Boolean); + + if (templateSegments.length !== actualSegments.length) { + return false; + } + + for (let index = 0; index < templateSegments.length; index += 1) { + const templateSegment = templateSegments[index]; + const actualSegment = actualSegments[index]; + + if (templateSegment.startsWith("{") && templateSegment.endsWith("}")) { + if (actualSegment.length === 0) return false; + continue; + } + + if (templateSegment !== actualSegment) { + return false; + } + } + + return true; +} + export function findEndpointByPathEffect( method: string, apiPath: string, @@ -158,9 +224,9 @@ export function findEndpointByPathEffect( > { return Effect.sync(() => { const upperMethod = method.toUpperCase(); - for (const domain of Object.values(DOMAIN_REGISTRY)) { + for (const domain of Object.values(domainRegistry)) { const endpoint = domain.endpoints.find( - (e) => e.method === upperMethod && e.path === apiPath, + (e) => e.method === upperMethod && pathTemplateMatches(e.path, apiPath), ); if (endpoint) return Option.some({ domain, endpoint }); } @@ -199,14 +265,21 @@ export function searchEndpointsEffect( endpoint: CatalogEndpoint; }> = []; - for (const domain of Object.values(DOMAIN_REGISTRY)) { + for (const domain of Object.values(domainRegistry)) { for (const endpoint of domain.endpoints) { + const graphqlSearchable = endpoint.graphql + ? endpoint.graphql.operations + .map((operation) => `${operation.kind} ${operation.name}`) + .join(" ") + : ""; + const searchable = [ endpoint.operationId, endpoint.summary, endpoint.description || "", endpoint.path, endpoint.method, + graphqlSearchable, ] .join(" ") .toLowerCase(); diff --git a/src/cli/schemas/api/manifest.json b/src/cli/schemas/api/manifest.json index 1e821ab..dac9e9c 100644 --- a/src/cli/schemas/api/manifest.json +++ b/src/cli/schemas/api/manifest.json @@ -1,6 +1,56 @@ { - "generated": "2026-02-26T02:05:00.692Z", + "generated": "2026-03-18T18:46:55.475Z", "domains": { + "bulk-operations": { + "file": "bulk-operations.json", + "title": "Bulk Operations API", + "endpointCount": 26 + }, + "businesses": { + "file": "businesses.json", + "title": "Commerce Business API", + "endpointCount": 8 + }, + "catalog-products": { + "file": "catalog-products.json", + "title": "Catalog GraphQL API", + "endpointCount": 1 + }, + "channels": { + "file": "channels.json", + "title": "Channels API", + "endpointCount": 8 + }, + "fulfillments": { + "file": "fulfillments.json", + "title": "Fulfillment Service", + "endpointCount": 3 + }, + "metafields": { + "file": "metafields.json", + "title": "Metafields API", + "endpointCount": 12 + }, + "onboarding": { + "file": "onboarding.json", + "title": "Commerce Onboarding API", + "endpointCount": 5 + }, + "orders": { + "file": "orders.json", + "title": "Order Service", + "endpointCount": 14 + }, + "stores": { + "file": "stores.json", + "title": "Commerce Store API", + "endpointCount": 8 + }, + "transactions": { + "file": "transactions.json", + "title": "Transactions API", + "endpointCount": 7 + }, "location-addresses": { "file": "location-addresses.json", "title": "Addresses API", diff --git a/src/cli/schemas/api/metafields.json b/src/cli/schemas/api/metafields.json new file mode 100644 index 0000000..22b8c85 --- /dev/null +++ b/src/cli/schemas/api/metafields.json @@ -0,0 +1,902 @@ +{ + "name": "metafields", + "title": "Metafields API", + "description": "This API allows for an extension of top-level commerce entities with custom data, providing the appropriate endpoints for managing commerce metafields.", + "version": "1.0.0", + "baseUrl": "https://api.godaddy.com/v1/commerce", + "endpoints": [ + { + "operationId": "getMetafields", + "method": "GET", + "path": "/stores/{storeId}/metafields", + "summary": "Get metafields", + "description": "This endpoint retrieves all the metafields for a given store using the store and resource IDs.", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "The store ID that metafields are scoped to", + "schema": { + "description": "A universally unique identifier (UUID) in [RFC-4122 format](https://tools.ietf.org/html/rfc4122).", + "type": "string", + "format": "uuid" + } + }, + { + "name": "pageSize", + "in": "query", + "required": false, + "description": "A non-negative, non-zero integer indicating the maximum number of resouces to return at one time", + "schema": { + "type": "integer", + "format": "int64", + "minimum": 1, + "maximum": 50, + "default": 25 + } + }, + { + "name": "pageToken", + "in": "query", + "required": false, + "description": "The cursor to use to fetch the next page of results", + "schema": { + "type": "string", + "description": "A string cursor representing a key or ID to use to fetch the next page of results", + "example": "MTY1MTc0NTczMDAwMDsyRzVWc1dFRmhJa3RIOFBZNHFDMGlPR0RwVko=" + } + }, + { + "name": "pageTokenDirection", + "in": "query", + "required": false, + "description": "Direction indicating whether to fetch the next page of results or the previous page of results", + "schema": { + "type": "string", + "description": "Page token direction", + "enum": ["BACKWARD", "FORWARD"], + "default": "FORWARD" + } + }, + { + "name": "resourceIds", + "in": "query", + "required": true, + "description": "A comma-delmited string of resourceIds to filter metafields by", + "schema": { + "type": "string", + "description": "A comma-delmited string of resourceIds" + } + }, + { + "name": "namespace", + "in": "query", + "required": false, + "description": "A namespace to filter metafields by", + "schema": { + "type": "string", + "description": "The namespace of the metafields to query by" + } + } + ], + "responses": { + "200": { + "description": "An array of metafields that are associated with the given store and resourceId", + "schema": { + "type": "object", + "properties": { + "metafields": { + "type": "array", + "items": { + "$ref": "#/components/schemas/metafield" + } + }, + "links": { + "type": "array", + "items": { + "title": "Link Description", + "type": "object", + "description": "A request-related [HATEOAS link](https://datatracker.ietf.org/doc/html/draft-handrews-json-schema-hyperschema-02).", + "properties": { + "href": { + "description": "The complete target URL, or link, to use in combination with the method to make the related call, as defined by [RFC 6570 - URI Template](https://tools.ietf.org/html/rfc6570), with the addition of the `$`, `(`, and `)` characters for pre-processing. The `href` is the key HATEOAS component that links a completed call with a subsequent call.", + "type": "string", + "format": "uri" + }, + "rel": { + "description": "The [link relation type](https://tools.ietf.org/html/rfc5988#section-4), which is an identifier for a link that unambiguously describes the semantics of the link. For values, see [Link Relationship Types](https://www.iana.org/assignments/link-relations/link-relations.xhtml).", + "type": "string" + }, + "title": { + "description": "The link title.", + "type": "string" + }, + "targetMediaType": { + "description": "The [RFC 2046-defined media type](https://www.ietf.org/rfc/rfc2046.txt) that describes the link target.", + "type": "string" + }, + "targetSchema": { + "description": "The schema that describes the link target." + }, + "method": { + "description": "The method to use to request the link target. For example, for HTTP, this might be `GET` or `DELETE`.", + "type": "string" + }, + "submissionMediaType": { + "description": "The media type with which to submit data with the request.", + "type": "string", + "default": "application/json" + }, + "submissionSchema": { + "description": "The schema that describes the request data." + } + }, + "required": ["rel", "href"] + } + } + } + } + }, + "401": { + "description": "See #/components/responses/401" + }, + "403": { + "description": "See #/components/responses/403" + }, + "404": { + "description": "See #/components/responses/404" + }, + "500": { + "description": "See #/components/responses/500" + } + }, + "scopes": ["commerce.metafield:read"] + }, + { + "operationId": "createMetafield", + "method": "POST", + "path": "/stores/{storeId}/metafields", + "summary": "Create metafield", + "description": "This endpoint creates a metafield and associates it with a store and resource using the store ID.", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "The store ID that metafields are scoped to", + "schema": { + "description": "A universally unique identifier (UUID) in [RFC-4122 format](https://tools.ietf.org/html/rfc4122).", + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "required": true, + "description": "The metafield to create", + "contentType": "application/json", + "schema": { + "$ref": "#/components/schemas/metafield-create-input" + } + }, + "responses": { + "200": { + "description": "Success response when creating a metafield", + "schema": { + "type": "object", + "properties": { + "metafield": { + "$ref": "#/components/schemas/metafield" + } + } + } + }, + "401": { + "description": "See #/components/responses/401" + }, + "403": { + "description": "See #/components/responses/403" + }, + "404": { + "description": "See #/components/responses/404" + }, + "500": { + "description": "See #/components/responses/500" + } + }, + "scopes": ["commerce.metafield:create"] + }, + { + "operationId": "bulkDeleteMetafields", + "method": "POST", + "path": "/stores/{storeId}/metafields/bulk-delete", + "summary": "Bulk deletes metafields", + "description": "This endpoint bulk deletes metafields by resourceId, namespace, and key.", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "The store ID that metafields are scoped to", + "schema": { + "description": "A universally unique identifier (UUID) in [RFC-4122 format](https://tools.ietf.org/html/rfc4122).", + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "required": true, + "description": "The metafields to delete", + "contentType": "application/json", + "schema": { + "$ref": "#/components/schemas/metafields-bulk-delete-input" + } + }, + "responses": { + "200": { + "description": "Success response when bulk deleting metafields", + "schema": { + "type": "object", + "properties": { + "metafieldIds": { + "type": "array", + "description": "The IDs of the deleted metafields", + "items": { + "type": "string", + "description": "The ID of the deleted metafield" + } + } + } + } + }, + "401": { + "description": "See #/components/responses/401" + }, + "403": { + "description": "See #/components/responses/403" + }, + "404": { + "description": "See #/components/responses/404" + }, + "500": { + "description": "See #/components/responses/500" + } + }, + "scopes": ["commerce.metafield:delete"] + }, + { + "operationId": "getMetafield", + "method": "GET", + "path": "/stores/{storeId}/metafields/{metafieldId}", + "summary": "Get a metafield", + "description": "Retrieve the information of a single metafield from a specific store using the store and metafield IDs.", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "The store ID that metafields are scoped to", + "schema": { + "description": "A universally unique identifier (UUID) in [RFC-4122 format](https://tools.ietf.org/html/rfc4122).", + "type": "string", + "format": "uuid" + } + }, + { + "name": "metafieldId", + "in": "path", + "required": true, + "description": "The ID of the metafield", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success response when getting a metafield", + "schema": { + "type": "object", + "properties": { + "metafield": { + "$ref": "#/components/schemas/metafield" + } + } + } + }, + "401": { + "description": "See #/components/responses/401" + }, + "403": { + "description": "See #/components/responses/403" + }, + "404": { + "description": "See #/components/responses/404" + }, + "500": { + "description": "See #/components/responses/500" + } + }, + "scopes": ["commerce.metafield:read"] + }, + { + "operationId": "deleteMetafield", + "method": "DELETE", + "path": "/stores/{storeId}/metafields/{metafieldId}", + "summary": "Delete a metafield", + "description": "Delete the information of a single metafield from a specific store using the store and metafield IDs.", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "The store ID that metafields are scoped to", + "schema": { + "description": "A universally unique identifier (UUID) in [RFC-4122 format](https://tools.ietf.org/html/rfc4122).", + "type": "string", + "format": "uuid" + } + }, + { + "name": "metafieldId", + "in": "path", + "required": true, + "description": "The ID of the metafield to delete", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success response when deleting a metafield", + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The ID of the deleted metafield" + }, + "status": { + "type": "string", + "description": "Status of the delete operation", + "enum": ["success", "failure"] + } + } + } + }, + "401": { + "description": "See #/components/responses/401" + }, + "403": { + "description": "See #/components/responses/403" + }, + "404": { + "description": "See #/components/responses/404" + }, + "500": { + "description": "See #/components/responses/500" + } + }, + "scopes": ["commerce.metafield:delete"] + }, + { + "operationId": "updateMetafield", + "method": "PUT", + "path": "/stores/{storeId}/metafields/{metafieldId}", + "summary": "Update a metafield", + "description": "Update the information of a single metafield using the store and metafield IDs.", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "The store ID that metafields are scoped to", + "schema": { + "description": "A universally unique identifier (UUID) in [RFC-4122 format](https://tools.ietf.org/html/rfc4122).", + "type": "string", + "format": "uuid" + } + }, + { + "name": "metafieldId", + "in": "path", + "required": true, + "description": "The ID of the metafield to update", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "description": "The metafield fields to update", + "contentType": "application/json", + "schema": { + "type": "object", + "properties": { + "metafield": { + "$ref": "#/components/schemas/metafield-update-input" + } + } + } + }, + "responses": { + "200": { + "description": "Success response when updating a metafield", + "schema": { + "type": "object", + "properties": { + "metafield": { + "$ref": "#/components/schemas/metafield" + } + } + } + }, + "401": { + "description": "See #/components/responses/401" + }, + "403": { + "description": "See #/components/responses/403" + }, + "404": { + "description": "See #/components/responses/404" + }, + "500": { + "description": "See #/components/responses/500" + } + }, + "scopes": ["commerce.metafield:update"] + }, + { + "operationId": "getMetafieldDefinitions", + "method": "GET", + "path": "/stores/{storeId}/metafield-definitions", + "summary": "Get metafield definition", + "description": "Retrieves all metafield definitions for a given store and resourceType", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "The store ID that metafields are scoped to", + "schema": { + "description": "A universally unique identifier (UUID) in [RFC-4122 format](https://tools.ietf.org/html/rfc4122).", + "type": "string", + "format": "uuid" + } + }, + { + "name": "pageSize", + "in": "query", + "required": false, + "description": "A non-negative, non-zero integer indicating the maximum number of resouces to return at one time", + "schema": { + "type": "integer", + "format": "int64", + "minimum": 1, + "maximum": 50, + "default": 25 + } + }, + { + "name": "pageToken", + "in": "query", + "required": false, + "description": "The cursor to use to fetch the next page of results", + "schema": { + "type": "string", + "description": "A string cursor representing a key or ID to use to fetch the next page of results", + "example": "MTY1MTc0NTczMDAwMDsyRzVWc1dFRmhJa3RIOFBZNHFDMGlPR0RwVko=" + } + }, + { + "name": "pageTokenDirection", + "in": "query", + "required": false, + "description": "Direction indicating whether to fetch the next page of results or the previous page of results", + "schema": { + "type": "string", + "description": "Page token direction", + "enum": ["BACKWARD", "FORWARD"], + "default": "FORWARD" + } + }, + { + "name": "resourceType", + "in": "query", + "required": true, + "description": "The resource type that the metafield definitions belong to", + "schema": { + "type": "string", + "description": "Resource type enum value", + "enum": [ + "STORE", + "CHANNEL", + "ORDER", + "PRODUCT", + "PRODUCT_VARIANT", + "CUSTOMER" + ] + } + }, + { + "name": "namespace", + "in": "query", + "required": false, + "description": "A namespace to filter metafields by", + "schema": { + "type": "string", + "description": "The namespace of the metafields to query by" + } + } + ], + "responses": { + "200": { + "description": "An array of metafield definitions that are associated with the given store and resourceType", + "schema": { + "type": "object", + "properties": { + "metafieldDefinitions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/metafield-definition" + } + }, + "links": { + "type": "array", + "items": { + "title": "Link Description", + "type": "object", + "description": "A request-related [HATEOAS link](https://datatracker.ietf.org/doc/html/draft-handrews-json-schema-hyperschema-02).", + "properties": { + "href": { + "description": "The complete target URL, or link, to use in combination with the method to make the related call, as defined by [RFC 6570 - URI Template](https://tools.ietf.org/html/rfc6570), with the addition of the `$`, `(`, and `)` characters for pre-processing. The `href` is the key HATEOAS component that links a completed call with a subsequent call.", + "type": "string", + "format": "uri" + }, + "rel": { + "description": "The [link relation type](https://tools.ietf.org/html/rfc5988#section-4), which is an identifier for a link that unambiguously describes the semantics of the link. For values, see [Link Relationship Types](https://www.iana.org/assignments/link-relations/link-relations.xhtml).", + "type": "string" + }, + "title": { + "description": "The link title.", + "type": "string" + }, + "targetMediaType": { + "description": "The [RFC 2046-defined media type](https://www.ietf.org/rfc/rfc2046.txt) that describes the link target.", + "type": "string" + }, + "targetSchema": { + "description": "The schema that describes the link target." + }, + "method": { + "description": "The method to use to request the link target. For example, for HTTP, this might be `GET` or `DELETE`.", + "type": "string" + }, + "submissionMediaType": { + "description": "The media type with which to submit data with the request.", + "type": "string", + "default": "application/json" + }, + "submissionSchema": { + "description": "The schema that describes the request data." + } + }, + "required": ["rel", "href"] + } + } + } + } + }, + "401": { + "description": "See #/components/responses/401" + }, + "403": { + "description": "See #/components/responses/403" + }, + "404": { + "description": "See #/components/responses/404" + }, + "500": { + "description": "See #/components/responses/500" + } + }, + "scopes": ["commerce.metafield:read"] + }, + { + "operationId": "createMetafieldDefinition", + "method": "POST", + "path": "/stores/{storeId}/metafield-definitions", + "summary": "Create a metafield definition", + "description": "This endpoint can be used to create a metafield definition and associate it with a store and resourceType", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "The store ID that metafields are scoped to", + "schema": { + "description": "A universally unique identifier (UUID) in [RFC-4122 format](https://tools.ietf.org/html/rfc4122).", + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "required": true, + "description": "The metafield definition to create", + "contentType": "application/json", + "schema": { + "type": "object", + "properties": { + "metafieldDefinition": { + "$ref": "#/components/schemas/metafield-definition-create-input" + } + } + } + }, + "responses": { + "200": { + "description": "Success response when creating a metafield definition", + "schema": { + "type": "object", + "properties": { + "metafieldDefinition": { + "$ref": "#/components/schemas/metafield-definition" + } + } + } + }, + "401": { + "description": "See #/components/responses/401" + }, + "403": { + "description": "See #/components/responses/403" + }, + "404": { + "description": "See #/components/responses/404" + }, + "500": { + "description": "See #/components/responses/500" + } + }, + "scopes": ["commerce.metafield:create"] + }, + { + "operationId": "getMetafieldDefinition", + "method": "GET", + "path": "/stores/{storeId}/metafield-definitions/{metafieldDefinitionId}", + "summary": "Get metafield definition by ID ", + "description": "This endpoint returns a single metafield definition using the metafield ID or resourceType and nampespace and key.", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "The store ID that metafields are scoped to", + "schema": { + "description": "A universally unique identifier (UUID) in [RFC-4122 format](https://tools.ietf.org/html/rfc4122).", + "type": "string", + "format": "uuid" + } + }, + { + "name": "metafieldDefinitionId", + "in": "path", + "required": true, + "description": "The metafield definition ID", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "A single metafield definition object", + "schema": { + "type": "object", + "properties": { + "metafieldDefinition": { + "$ref": "#/components/schemas/metafield-definition" + } + } + } + }, + "401": { + "description": "See #/components/responses/401" + }, + "403": { + "description": "See #/components/responses/403" + }, + "404": { + "description": "See #/components/responses/404" + }, + "500": { + "description": "See #/components/responses/500" + } + }, + "scopes": ["commerce.metafield:read"] + }, + { + "operationId": "updateMetafieldDefinition", + "method": "PUT", + "path": "/stores/{storeId}/metafield-definitions/{metafieldDefinitionId}", + "summary": "Update a metafield definition", + "description": "This endpoint can be used to update a single metafield definition using the store and metafieldDefinition IDs.", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "The store ID that metafields are scoped to", + "schema": { + "description": "A universally unique identifier (UUID) in [RFC-4122 format](https://tools.ietf.org/html/rfc4122).", + "type": "string", + "format": "uuid" + } + }, + { + "name": "metafieldDefinitionId", + "in": "path", + "required": true, + "description": "The ID of the metafield definition to update", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "description": "The metafield definition fields to update", + "contentType": "application/json", + "schema": { + "type": "object", + "properties": { + "metafield": { + "$ref": "#/components/schemas/metafield-definition-update-input" + } + } + } + }, + "responses": { + "200": { + "description": "Success response when updating a metafield definition", + "schema": { + "type": "object", + "properties": { + "metafield": { + "$ref": "#/components/schemas/metafield-definition" + } + } + } + }, + "401": { + "description": "See #/components/responses/401" + }, + "403": { + "description": "See #/components/responses/403" + }, + "404": { + "description": "See #/components/responses/404" + }, + "500": { + "description": "See #/components/responses/500" + } + }, + "scopes": ["commerce.metafield:update"] + }, + { + "operationId": "deleteMetafieldDefinition", + "method": "DELETE", + "path": "/stores/{storeId}/metafield-definitions/{metafieldDefinitionId}", + "summary": "Delete a metafield", + "description": "This endpoint can be used to delete a single metafield definition using the store and metafieldDefinition IDs.", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "The store ID that metafields are scoped to", + "schema": { + "description": "A universally unique identifier (UUID) in [RFC-4122 format](https://tools.ietf.org/html/rfc4122).", + "type": "string", + "format": "uuid" + } + }, + { + "name": "metafieldDefinitionId", + "in": "path", + "required": true, + "description": "The metafield definition ID", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "A single metafield definition object", + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The ID of the deleted metafield definition" + }, + "status": { + "type": "string", + "description": "The status of the deleted metafield definition", + "enum": ["success", "error"] + } + } + } + }, + "401": { + "description": "See #/components/responses/401" + }, + "403": { + "description": "See #/components/responses/403" + }, + "404": { + "description": "See #/components/responses/404" + }, + "500": { + "description": "See #/components/responses/500" + } + }, + "scopes": ["commerce.metafield:delete"] + }, + { + "operationId": "getMetafieldDefinitionTypes", + "method": "GET", + "path": "/stores/{storeId}/metafield-definition-types", + "summary": "Get all metafield definition types", + "description": "This endpoint can be used to retrieve all the metafield definition types available using the store ID.", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "The store ID that metafields are scoped to", + "schema": { + "description": "A universally unique identifier (UUID) in [RFC-4122 format](https://tools.ietf.org/html/rfc4122).", + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "An array of metafield definition types that are available for clients to use when creating metafield definitions and values", + "schema": { + "type": "object", + "properties": { + "metafieldDefinitionTypes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/metafield-definition-type" + } + } + } + } + }, + "401": { + "description": "See #/components/responses/401" + }, + "403": { + "description": "See #/components/responses/403" + }, + "404": { + "description": "See #/components/responses/404" + }, + "500": { + "description": "See #/components/responses/500" + } + }, + "scopes": ["commerce.metafield:read"] + } + ] +} diff --git a/src/cli/schemas/api/onboarding.json b/src/cli/schemas/api/onboarding.json new file mode 100644 index 0000000..900be33 --- /dev/null +++ b/src/cli/schemas/api/onboarding.json @@ -0,0 +1,219 @@ +{ + "name": "onboarding", + "title": "Commerce Onboarding API", + "description": "

Onboarding platform facilitates onboarding a merchant onto GoDaddy's omni-channel products.

To onboard a merchant, various steps are taken to verify the owner information which permits access to payments, orders and GoDaddy products.", + "version": "1.0.0", + "baseUrl": "https://api.godaddy.com/v1/commerce", + "endpoints": [ + { + "operationId": "getOnboardingApplications", + "method": "GET", + "path": "/onboarding-applications", + "summary": "Get onboarding applications", + "description": "Returns a list of existing onboarding applications", + "parameters": [ + { + "name": "businessId", + "in": "query", + "required": false, + "description": "Unique ID for the business", + "schema": { + "type": "string" + } + }, + { + "name": "storeId", + "in": "query", + "required": false, + "description": "Unique ID for the store", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "List of onboarding applications", + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/PagedList" + } + ], + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "./models/Application.yaml" + } + } + } + } + } + }, + "scopes": ["commerce.onboarding:read", "commerce.onboarding:write"] + }, + { + "operationId": "createApplication", + "method": "POST", + "path": "/onboarding-applications", + "summary": "Create an onboarding application", + "description": "Creates an application and start the onboarding process", + "parameters": [ + { + "name": "mock", + "in": "query", + "required": false, + "description": "Test account", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "name": "onboard", + "in": "query", + "required": false, + "description": "When false, the application will be created without processing", + "schema": { + "type": "boolean", + "default": true + } + } + ], + "requestBody": { + "required": false, + "contentType": "application/json", + "schema": { + "$ref": "./models/Application.yaml" + } + }, + "responses": { + "201": { + "description": "Onboarding Application has been created successfully", + "schema": { + "$ref": "./models/Application.yaml" + } + }, + "400": { + "description": "See #/components/responses/badRequest" + } + }, + "scopes": ["commerce.onboarding:write"] + }, + { + "operationId": "getOnboardingApplication", + "method": "GET", + "path": "/onboarding-applications/{onboardingApplicationId}", + "summary": "Get an onboarding application", + "description": "Returns an onboarding application by id", + "parameters": [ + { + "name": "onboardingApplicationId", + "in": "path", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Found onboarding application", + "schema": { + "allOf": [ + { + "$ref": "./models/Application.yaml" + } + ], + "properties": { + "links": { + "type": "array", + "description": "An array of error-related HATEOAS links.", + "readOnly": true, + "items": { + "$ref": "./common-types/link-description.json" + } + } + } + } + }, + "404": { + "description": "See #/components/responses/notFound" + } + }, + "scopes": ["commerce.onboarding:read", "commerce.onboarding:write"] + }, + { + "operationId": "patchOnboardingApplication", + "method": "PATCH", + "path": "/onboarding-applications/{onboardingApplicationId}", + "summary": "Patch an onboarding application", + "description": "Updates an application by id", + "parameters": [ + { + "name": "onboardingApplicationId", + "in": "path", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": false, + "contentType": "application/json", + "schema": { + "$ref": "./common-types/patch-request.json" + } + }, + "responses": { + "200": { + "description": "Updated onboarding application", + "schema": { + "$ref": "./models/Application.yaml" + } + }, + "400": { + "description": "See #/components/responses/badRequest" + }, + "404": { + "description": "See #/components/responses/notFound" + } + }, + "scopes": ["commerce.onboarding:write"] + }, + { + "operationId": "patchOnboardingApplication", + "method": "POST", + "path": "/onboarding-applications/{onboardingApplicationId}/onboard", + "summary": "Start onboarding a merchant", + "description": "Start the onboarding process for an existing application", + "parameters": [ + { + "name": "onboardingApplicationId", + "in": "path", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Onboarded an existing onboarding application", + "schema": { + "$ref": "./models/Application.yaml" + } + }, + "400": { + "description": "See #/components/responses/badRequest" + }, + "404": { + "description": "See #/components/responses/notFound" + } + }, + "scopes": ["commerce.onboarding:write"] + } + ] +} diff --git a/src/cli/schemas/api/orders.json b/src/cli/schemas/api/orders.json new file mode 100644 index 0000000..1445fb6 --- /dev/null +++ b/src/cli/schemas/api/orders.json @@ -0,0 +1,1379 @@ +{ + "name": "orders", + "title": "Order Service", + "description": "This API can be used for creating, updating, canceling and retrieving orders as well as archiving and retrieving archived orders related to a store.", + "version": "1.10.0", + "baseUrl": "https://api.godaddy.com", + "endpoints": [ + { + "operationId": "createOrder", + "method": "POST", + "path": "/v1/commerce/stores/{storeId}/orders", + "summary": "Create a new order", + "description": "Create a new order", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "Store ID", + "schema": { + "$ref": "./models/Order.yaml#/properties/context/properties/storeId" + } + } + ], + "requestBody": { + "required": true, + "description": "The order object to create", + "contentType": "application/json", + "schema": { + "$ref": "#/components/schemas/Order" + } + }, + "responses": { + "200": { + "description": "Successfully created the order", + "schema": { + "type": "object", + "title": "OrderCreated", + "required": ["status", "order"], + "properties": { + "status": { + "type": "string", + "description": "Status of the response", + "example": "success" + }, + "order": { + "$ref": "#/components/schemas/Order" + } + } + } + }, + "401": { + "description": "See #/components/responses/401" + }, + "403": { + "description": "See #/components/responses/403" + }, + "404": { + "description": "See #/components/responses/404" + }, + "409": { + "description": "See #/components/responses/409" + }, + "422": { + "description": "See #/components/responses/422" + }, + "500": { + "description": "See #/components/responses/500" + }, + "502": { + "description": "See #/components/responses/502" + }, + "503": { + "description": "See #/components/responses/503" + }, + "504": { + "description": "See #/components/responses/504" + } + }, + "scopes": ["commerce.order:create"] + }, + { + "operationId": "putOrder", + "method": "PUT", + "path": "/v1/commerce/stores/{storeId}/orders", + "summary": "Create order with identifier", + "description": "Create an order with a specified identifier", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "Store ID", + "schema": { + "$ref": "./models/Order.yaml#/properties/context/properties/storeId" + } + } + ], + "requestBody": { + "required": true, + "description": "The order object to create with a passed in Global ID (e.g. `Order_`)", + "contentType": "application/json", + "schema": { + "$ref": "#/components/schemas/Order" + } + }, + "responses": { + "200": { + "description": "Successfully created the order", + "schema": { + "type": "object", + "title": "OrderWithIdCreated", + "required": ["status", "order"], + "properties": { + "status": { + "type": "string", + "description": "Status of the response", + "example": "success" + }, + "order": { + "$ref": "#/components/schemas/Order" + } + } + } + }, + "401": { + "description": "See #/components/responses/401" + }, + "403": { + "description": "See #/components/responses/403" + }, + "404": { + "description": "See #/components/responses/404" + }, + "409": { + "description": "See #/components/responses/409" + }, + "422": { + "description": "See #/components/responses/422" + }, + "500": { + "description": "See #/components/responses/500" + }, + "502": { + "description": "See #/components/responses/502" + }, + "503": { + "description": "See #/components/responses/503" + }, + "504": { + "description": "See #/components/responses/504" + } + }, + "scopes": ["commerce.order:create"] + }, + { + "operationId": "queryOrders", + "method": "GET", + "path": "/v1/commerce/stores/{storeId}/orders", + "summary": "Find and filter orders", + "description": "Find orders and filter them using the criteria provided", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "Store ID", + "schema": { + "$ref": "./models/Order.yaml#/properties/context/properties/storeId" + } + }, + { + "name": "pageSize", + "in": "query", + "required": false, + "description": "A non-negative, non-zero integer indicating the maximum number of resouces to return at one time", + "schema": { + "type": "integer", + "format": "int64", + "minimum": 1, + "maximum": 100, + "default": 25 + } + }, + { + "name": "pageToken", + "in": "query", + "required": false, + "description": "Returns the resources that come after this specified cursor", + "schema": { + "type": "string", + "description": "A cursor for use in pagination", + "example": "MTY1MTc0NTczMDAwMDsyRzVWc1dFRmhJa3RIOFBZNHFDMGlPR0RwVko=" + } + }, + { + "name": "pageTokenDirection", + "in": "query", + "required": false, + "description": "Direction indicator used for cursor based pagination", + "schema": { + "type": "string", + "description": "Page token direction", + "enum": ["BACKWARD", "FORWARD"], + "default": "FORWARD" + } + }, + { + "name": "processedAtStart", + "in": "query", + "required": false, + "description": "Grab orders with their processedAt => this datetime", + "schema": { + "description": "A date and time, in [Internet date and time format](https://tools.ietf.org/html/rfc3339#section-5.6). Note: The regular expression provides static schematic guidance but does not reject all invalid dates.", + "type": "string", + "minLength": 20, + "maxLength": 64, + "pattern": "^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])[T,t]([0-1][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)([.][0-9]+)?([Zz]|[+-][0-9]{2}:[0-9]{2})$" + } + }, + { + "name": "processedAtEnd", + "in": "query", + "required": false, + "description": "Grab orders with their processedAt <= this datetime", + "schema": { + "description": "A date and time, in [Internet date and time format](https://tools.ietf.org/html/rfc3339#section-5.6). Note: The regular expression provides static schematic guidance but does not reject all invalid dates.", + "type": "string", + "minLength": 20, + "maxLength": 64, + "pattern": "^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])[T,t]([0-1][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)([.][0-9]+)?([Zz]|[+-][0-9]{2}:[0-9]{2})$" + } + }, + { + "name": "updatedAtStart", + "in": "query", + "required": false, + "description": "Grab orders with their updatedAt => this datetime", + "schema": { + "description": "A date and time, in [Internet date and time format](https://tools.ietf.org/html/rfc3339#section-5.6). Note: The regular expression provides static schematic guidance but does not reject all invalid dates.", + "type": "string", + "minLength": 20, + "maxLength": 64, + "pattern": "^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])[T,t]([0-1][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)([.][0-9]+)?([Zz]|[+-][0-9]{2}:[0-9]{2})$" + } + }, + { + "name": "updatedAtEnd", + "in": "query", + "required": false, + "description": "Grab orders with their updatedAt <= this datetime", + "schema": { + "description": "A date and time, in [Internet date and time format](https://tools.ietf.org/html/rfc3339#section-5.6). Note: The regular expression provides static schematic guidance but does not reject all invalid dates.", + "type": "string", + "minLength": 20, + "maxLength": 64, + "pattern": "^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])[T,t]([0-1][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)([.][0-9]+)?([Zz]|[+-][0-9]{2}:[0-9]{2})$" + } + }, + { + "name": "status", + "in": "query", + "required": false, + "description": "Order status (comma separated)", + "schema": { + "type": "string", + "description": "List of order statuses (comma separated)", + "example": "OPEN,COMPLETED" + } + }, + { + "name": "paymentStatus", + "in": "query", + "required": false, + "description": "Order payment statuses (comma separated)", + "schema": { + "type": "string", + "description": "List of order payment statuses (comma separated)", + "example": "PROCESSING,PAID" + } + }, + { + "name": "fulfillmentStatus", + "in": "query", + "required": false, + "description": "Order fulfillment statuses (comma separated)", + "schema": { + "type": "string", + "description": "List of order fulfillment statuses (comma separated)", + "example": "UNFULFILLED,IN_PROGRESS" + } + }, + { + "name": "fulfillmentMode", + "in": "query", + "required": false, + "description": "Line item fulfillment mode - taken from line items (comma separated)", + "schema": { + "type": "string", + "description": "List of line item fulfillment modes (comma separated)", + "example": "PICKUP,SHIP" + } + }, + { + "name": "channelId", + "in": "query", + "required": false, + "description": "Channel ID(s) to filter orders by", + "schema": { + "type": "string", + "description": "List of order channelId (comma separated)", + "example": "7a65506f-d706-420b-a5ec-25c5bf69d000,0349f033-a556-49b7-9233-4edd39540112" + } + }, + { + "name": "name", + "in": "query", + "required": false, + "description": "Shipping / billing name to filter orders by", + "schema": { + "type": "string", + "description": "Shipping / billing name", + "example": "Bob Loblaw" + } + }, + { + "name": "email", + "in": "query", + "required": false, + "description": "Shipping / billing email(s) to filter orders by", + "schema": { + "type": "string", + "description": "Email(s)- comma separated", + "example": "bobloblaw@gmail.com,dirk-nowitzki@gmail.com" + } + }, + { + "name": "tags", + "in": "query", + "required": false, + "description": "Order tags to filter orders by", + "schema": { + "type": "string", + "description": "List of tags (comma separated)", + "example": "digital,poynt" + } + }, + { + "name": "sortBy", + "in": "query", + "required": false, + "description": "Sort the underlying orders by the given key", + "schema": { + "type": "string", + "description": "Key to sort orders by", + "enum": [ + "number", + "processedAt", + "total", + "status", + "fulfillmentStatus", + "paymentStatus", + "updatedAt" + ], + "default": "processedAt" + } + }, + { + "name": "sortOrder", + "in": "query", + "required": false, + "description": "Sort direction", + "schema": { + "type": "string", + "description": "Sort direction", + "enum": ["ASC", "DESC"], + "default": "ASC" + } + } + ], + "responses": { + "200": { + "description": "Successfully listed orders", + "schema": { + "type": "object", + "title": "Orders", + "required": ["status", "orders", "links"], + "properties": { + "status": { + "type": "string", + "description": "Status of the response", + "example": "success" + }, + "orders": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Order" + } + }, + "links": { + "type": "array", + "description": "Array containing one or more HATEOAS link relations that are relevant for traversing orders", + "items": { + "title": "Link Description", + "type": "object", + "description": "A request-related [HATEOAS link](https://datatracker.ietf.org/doc/html/draft-handrews-json-schema-hyperschema-02).", + "properties": { + "href": { + "description": "The complete target URL, or link, to use in combination with the method to make the related call, as defined by [RFC 6570 - URI Template](https://tools.ietf.org/html/rfc6570), with the addition of the `$`, `(`, and `)` characters for pre-processing. The `href` is the key HATEOAS component that links a completed call with a subsequent call.", + "type": "string", + "format": "uri" + }, + "rel": { + "description": "The [link relation type](https://tools.ietf.org/html/rfc5988#section-4), which is an identifier for a link that unambiguously describes the semantics of the link. For values, see [Link Relationship Types](https://www.iana.org/assignments/link-relations/link-relations.xhtml).", + "type": "string" + }, + "title": { + "description": "The link title.", + "type": "string" + }, + "targetMediaType": { + "description": "The [RFC 2046-defined media type](https://www.ietf.org/rfc/rfc2046.txt) that describes the link target.", + "type": "string" + }, + "targetSchema": { + "description": "The schema that describes the link target." + }, + "method": { + "description": "The method to use to request the link target. For example, for HTTP, this might be `GET` or `DELETE`.", + "type": "string" + }, + "submissionMediaType": { + "description": "The media type with which to submit data with the request.", + "type": "string", + "default": "application/json" + }, + "submissionSchema": { + "description": "The schema that describes the request data." + } + }, + "required": ["rel", "href"] + } + } + } + } + }, + "401": { + "description": "See #/components/responses/401" + }, + "403": { + "description": "See #/components/responses/403" + }, + "404": { + "description": "See #/components/responses/404" + }, + "409": { + "description": "See #/components/responses/409" + }, + "422": { + "description": "See #/components/responses/422" + }, + "500": { + "description": "See #/components/responses/500" + }, + "502": { + "description": "See #/components/responses/502" + }, + "503": { + "description": "See #/components/responses/503" + }, + "504": { + "description": "See #/components/responses/504" + } + }, + "scopes": ["commerce.order:read"] + }, + { + "operationId": "findOrder", + "method": "GET", + "path": "/v1/commerce/stores/{storeId}/orders/{orderId}", + "summary": "Find an order", + "description": "Find a single order using the order ID.", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "Store ID", + "schema": { + "$ref": "./models/Order.yaml#/properties/context/properties/storeId" + } + }, + { + "name": "orderId", + "in": "path", + "required": true, + "description": "The ID of the order", + "schema": { + "$ref": "./models/Order.yaml#/properties/id" + } + } + ], + "responses": { + "200": { + "description": "Successfully found the order", + "schema": { + "type": "object", + "required": ["status", "order"], + "properties": { + "status": { + "type": "string", + "description": "Status of the response", + "example": "success" + }, + "order": { + "$ref": "#/components/schemas/Order" + } + } + } + }, + "401": { + "description": "See #/components/responses/401" + }, + "403": { + "description": "See #/components/responses/403" + }, + "404": { + "description": "See #/components/responses/404" + }, + "409": { + "description": "See #/components/responses/409" + }, + "422": { + "description": "See #/components/responses/422" + }, + "500": { + "description": "See #/components/responses/500" + }, + "502": { + "description": "See #/components/responses/502" + }, + "503": { + "description": "See #/components/responses/503" + }, + "504": { + "description": "See #/components/responses/504" + } + }, + "scopes": ["commerce.order:read"] + }, + { + "operationId": "patchOrder", + "method": "PATCH", + "path": "/v1/commerce/stores/{storeId}/orders/{orderId}", + "summary": "Patch an order", + "description": "Modify an existing order using the store and order ID.", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "Store ID", + "schema": { + "$ref": "./models/Order.yaml#/properties/context/properties/storeId" + } + }, + { + "name": "orderId", + "in": "path", + "required": true, + "description": "The ID of the order", + "schema": { + "$ref": "./models/Order.yaml#/properties/id" + } + } + ], + "requestBody": { + "required": true, + "description": "The order object to patch", + "contentType": "application/json", + "schema": { + "$ref": "./models/OrderPatch.yaml" + } + }, + "responses": { + "200": { + "description": "Successfully patched the order", + "schema": { + "type": "object", + "required": ["status", "order"], + "properties": { + "status": { + "type": "string", + "description": "Status of the response", + "example": "success" + }, + "order": { + "$ref": "#/components/schemas/Order" + } + } + } + }, + "401": { + "description": "See #/components/responses/401" + }, + "403": { + "description": "See #/components/responses/403" + }, + "404": { + "description": "See #/components/responses/404" + }, + "409": { + "description": "See #/components/responses/409" + }, + "422": { + "description": "See #/components/responses/422" + }, + "500": { + "description": "See #/components/responses/500" + }, + "502": { + "description": "See #/components/responses/502" + }, + "503": { + "description": "See #/components/responses/503" + }, + "504": { + "description": "See #/components/responses/504" + } + }, + "scopes": ["commerce.order:update"] + }, + { + "operationId": "queryArchivedOrders", + "method": "GET", + "path": "/v1/commerce/stores/{storeId}/orders/archived", + "summary": "Find archived orders", + "description": "Find any archived orders with pagination using the store ID.", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "Store ID", + "schema": { + "$ref": "./models/Order.yaml#/properties/context/properties/storeId" + } + }, + { + "name": "pageSize", + "in": "query", + "required": false, + "description": "A non-negative, non-zero integer indicating the maximum number of resouces to return at one time", + "schema": { + "type": "integer", + "format": "int64", + "minimum": 1, + "maximum": 100, + "default": 25 + } + }, + { + "name": "pageToken", + "in": "query", + "required": false, + "description": "Returns the resources that come after this specified cursor", + "schema": { + "type": "string", + "description": "A cursor for use in pagination", + "example": "MTY1MTc0NTczMDAwMDsyRzVWc1dFRmhJa3RIOFBZNHFDMGlPR0RwVko=" + } + }, + { + "name": "pageTokenDirection", + "in": "query", + "required": false, + "description": "Direction indicator used for cursor based pagination", + "schema": { + "type": "string", + "description": "Page token direction", + "enum": ["BACKWARD", "FORWARD"], + "default": "FORWARD" + } + } + ], + "responses": { + "200": { + "description": "Successfully listed archived orders", + "schema": { + "type": "object", + "title": "ArchivedOrders", + "required": ["status", "orders", "links"], + "properties": { + "status": { + "type": "string", + "description": "Status of the response", + "example": "success" + }, + "orders": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Order" + } + }, + "links": { + "type": "array", + "description": "Array containing one or more HATEOAS link relations that are relevant for traversing archived orders", + "items": { + "title": "Link Description", + "type": "object", + "description": "A request-related [HATEOAS link](https://datatracker.ietf.org/doc/html/draft-handrews-json-schema-hyperschema-02).", + "properties": { + "href": { + "description": "The complete target URL, or link, to use in combination with the method to make the related call, as defined by [RFC 6570 - URI Template](https://tools.ietf.org/html/rfc6570), with the addition of the `$`, `(`, and `)` characters for pre-processing. The `href` is the key HATEOAS component that links a completed call with a subsequent call.", + "type": "string", + "format": "uri" + }, + "rel": { + "description": "The [link relation type](https://tools.ietf.org/html/rfc5988#section-4), which is an identifier for a link that unambiguously describes the semantics of the link. For values, see [Link Relationship Types](https://www.iana.org/assignments/link-relations/link-relations.xhtml).", + "type": "string" + }, + "title": { + "description": "The link title.", + "type": "string" + }, + "targetMediaType": { + "description": "The [RFC 2046-defined media type](https://www.ietf.org/rfc/rfc2046.txt) that describes the link target.", + "type": "string" + }, + "targetSchema": { + "description": "The schema that describes the link target." + }, + "method": { + "description": "The method to use to request the link target. For example, for HTTP, this might be `GET` or `DELETE`.", + "type": "string" + }, + "submissionMediaType": { + "description": "The media type with which to submit data with the request.", + "type": "string", + "default": "application/json" + }, + "submissionSchema": { + "description": "The schema that describes the request data." + } + }, + "required": ["rel", "href"] + } + } + } + } + }, + "401": { + "description": "See #/components/responses/401" + }, + "403": { + "description": "See #/components/responses/403" + }, + "404": { + "description": "See #/components/responses/404" + }, + "409": { + "description": "See #/components/responses/409" + }, + "422": { + "description": "See #/components/responses/422" + }, + "500": { + "description": "See #/components/responses/500" + }, + "502": { + "description": "See #/components/responses/502" + }, + "503": { + "description": "See #/components/responses/503" + }, + "504": { + "description": "See #/components/responses/504" + } + }, + "scopes": ["commerce.order:read"] + }, + { + "operationId": "archiveOrder", + "method": "POST", + "path": "/v1/commerce/stores/{storeId}/orders/{orderId}/archive", + "summary": "Archive an order", + "description": "Archive an order using the store and order IDs.", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "Store ID", + "schema": { + "$ref": "./models/Order.yaml#/properties/context/properties/storeId" + } + }, + { + "name": "orderId", + "in": "path", + "required": true, + "description": "The ID of the order", + "schema": { + "$ref": "./models/Order.yaml#/properties/id" + } + } + ], + "responses": { + "200": { + "description": "Successfully archived the order", + "schema": { + "type": "object", + "required": ["status", "order"], + "properties": { + "status": { + "type": "string", + "description": "Status of the response", + "example": "success" + }, + "order": { + "$ref": "#/components/schemas/Order" + } + } + } + }, + "401": { + "description": "See #/components/responses/401" + }, + "403": { + "description": "See #/components/responses/403" + }, + "404": { + "description": "See #/components/responses/404" + }, + "409": { + "description": "See #/components/responses/409" + }, + "422": { + "description": "See #/components/responses/422" + }, + "500": { + "description": "See #/components/responses/500" + }, + "502": { + "description": "See #/components/responses/502" + }, + "503": { + "description": "See #/components/responses/503" + }, + "504": { + "description": "See #/components/responses/504" + } + }, + "scopes": ["commerce.order:archive"] + }, + { + "operationId": "cancelOrder", + "method": "POST", + "path": "/v1/commerce/stores/{storeId}/orders/{orderId}/cancel", + "summary": "Cancel an order", + "description": "Cancel an order using the store and order IDs.", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "Store ID", + "schema": { + "$ref": "./models/Order.yaml#/properties/context/properties/storeId" + } + }, + { + "name": "orderId", + "in": "path", + "required": true, + "description": "The ID of the order", + "schema": { + "$ref": "./models/Order.yaml#/properties/id" + } + } + ], + "responses": { + "200": { + "description": "Successfully canceled the order", + "schema": { + "type": "object", + "required": ["status", "order"], + "properties": { + "status": { + "type": "string", + "description": "Status of the response", + "example": "success" + }, + "order": { + "$ref": "#/components/schemas/Order" + } + } + } + }, + "401": { + "description": "See #/components/responses/401" + }, + "403": { + "description": "See #/components/responses/403" + }, + "404": { + "description": "See #/components/responses/404" + }, + "409": { + "description": "See #/components/responses/409" + }, + "422": { + "description": "See #/components/responses/422" + }, + "500": { + "description": "See #/components/responses/500" + }, + "502": { + "description": "See #/components/responses/502" + }, + "503": { + "description": "See #/components/responses/503" + }, + "504": { + "description": "See #/components/responses/504" + } + }, + "scopes": ["commerce.order:cancel"] + }, + { + "operationId": "completeOrder", + "method": "POST", + "path": "/v1/commerce/stores/{storeId}/orders/{orderId}/complete", + "summary": "Complete an order", + "description": "Complete an order using the store and order IDs.", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "Store ID", + "schema": { + "$ref": "./models/Order.yaml#/properties/context/properties/storeId" + } + }, + { + "name": "orderId", + "in": "path", + "required": true, + "description": "The ID of the order", + "schema": { + "$ref": "./models/Order.yaml#/properties/id" + } + } + ], + "responses": { + "200": { + "description": "Successfully completed the order", + "schema": { + "type": "object", + "required": ["status", "order"], + "properties": { + "status": { + "type": "string", + "description": "Status of the response", + "example": "success" + }, + "order": { + "$ref": "#/components/schemas/Order" + } + } + } + }, + "401": { + "description": "See #/components/responses/401" + }, + "403": { + "description": "See #/components/responses/403" + }, + "404": { + "description": "See #/components/responses/404" + }, + "409": { + "description": "See #/components/responses/409" + }, + "422": { + "description": "See #/components/responses/422" + }, + "500": { + "description": "See #/components/responses/500" + }, + "502": { + "description": "See #/components/responses/502" + }, + "503": { + "description": "See #/components/responses/503" + }, + "504": { + "description": "See #/components/responses/504" + } + }, + "scopes": ["commerce.order:complete"] + }, + { + "operationId": "createReturn", + "method": "POST", + "path": "/v1/commerce/stores/{storeId}/returns", + "summary": "Create a return for an order", + "description": "Create a return for an order", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "Store ID", + "schema": { + "$ref": "./models/Order.yaml#/properties/context/properties/storeId" + } + } + ], + "requestBody": { + "required": true, + "description": "The Return object to create", + "contentType": "application/json", + "schema": { + "$ref": "#/components/schemas/Return" + } + }, + "responses": { + "200": { + "description": "Successfully created the return", + "schema": { + "$ref": "#/components/schemas/Return" + } + }, + "401": { + "description": "See #/components/responses/401" + }, + "403": { + "description": "See #/components/responses/403" + }, + "404": { + "description": "See #/components/responses/404" + }, + "409": { + "description": "See #/components/responses/409" + }, + "422": { + "description": "See #/components/responses/422" + }, + "500": { + "description": "See #/components/responses/500" + }, + "502": { + "description": "See #/components/responses/502" + }, + "503": { + "description": "See #/components/responses/503" + }, + "504": { + "description": "See #/components/responses/504" + } + }, + "scopes": ["commerce.return:create"] + }, + { + "operationId": "findReturn", + "method": "GET", + "path": "/v1/commerce/stores/{storeId}/returns/{returnId}", + "summary": "Find a return", + "description": "Find a single return.", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "Store ID", + "schema": { + "$ref": "./models/Order.yaml#/properties/context/properties/storeId" + } + }, + { + "name": "returnId", + "in": "path", + "required": true, + "description": "The ID of the return", + "schema": { + "type": "string", + "format": "uuid", + "description": "UUID of the return" + } + } + ], + "responses": { + "200": { + "description": "Successfully found the return", + "schema": { + "$ref": "#/components/schemas/Return" + } + }, + "401": { + "description": "See #/components/responses/401" + }, + "403": { + "description": "See #/components/responses/403" + }, + "404": { + "description": "See #/components/responses/404" + }, + "409": { + "description": "See #/components/responses/409" + }, + "422": { + "description": "See #/components/responses/422" + }, + "500": { + "description": "See #/components/responses/500" + }, + "502": { + "description": "See #/components/responses/502" + }, + "503": { + "description": "See #/components/responses/503" + }, + "504": { + "description": "See #/components/responses/504" + } + }, + "scopes": ["commerce.return:read"] + }, + { + "operationId": "patchReturn", + "method": "PATCH", + "path": "/v1/commerce/stores/{storeId}/returns/{returnId}", + "summary": "Modify an existing return", + "description": "Modify an existing return", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "Store ID", + "schema": { + "$ref": "./models/Order.yaml#/properties/context/properties/storeId" + } + }, + { + "name": "returnId", + "in": "path", + "required": true, + "description": "The ID of the return", + "schema": { + "type": "string", + "format": "uuid", + "description": "UUID of the return" + } + } + ], + "requestBody": { + "required": true, + "description": "The return object to patch", + "contentType": "application/json", + "schema": { + "type": "array", + "items": { + "type": "object", + "title": "Patch", + "description": "The JSON patch object to apply partial updates to resources.", + "properties": { + "op": { + "type": "string", + "description": "The operation to complete.", + "enum": ["add", "remove", "replace", "move", "copy", "test"] + }, + "path": { + "type": "string", + "description": "The JSON pointer to the target document location at which to complete the operation." + }, + "value": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "integer" + }, + { + "type": "string" + }, + { + "type": "boolean" + }, + { + "type": "array", + "items": {} + }, + { + "type": "object" + } + ], + "nullable": true, + "description": "The value to apply. The `remove` operation does not require a value." + }, + "from": { + "type": "string", + "description": "The JSON pointer to the target document location from which to move the value. Required for the `move` operation." + } + }, + "required": ["op"] + } + } + }, + "responses": { + "200": { + "description": "Successfully patched the return status", + "schema": { + "$ref": "#/components/schemas/Return" + } + }, + "401": { + "description": "See #/components/responses/401" + }, + "403": { + "description": "See #/components/responses/403" + }, + "404": { + "description": "See #/components/responses/404" + }, + "409": { + "description": "See #/components/responses/409" + }, + "422": { + "description": "See #/components/responses/422" + }, + "500": { + "description": "See #/components/responses/500" + }, + "502": { + "description": "See #/components/responses/502" + }, + "503": { + "description": "See #/components/responses/503" + }, + "504": { + "description": "See #/components/responses/504" + } + }, + "scopes": ["commerce.return:update"] + }, + { + "operationId": "deleteReturn", + "method": "DELETE", + "path": "/v1/commerce/stores/{storeId}/returns/{returnId}", + "summary": "Delete a return", + "description": "Delete a single return.", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "Store ID", + "schema": { + "$ref": "./models/Order.yaml#/properties/context/properties/storeId" + } + }, + { + "name": "returnId", + "in": "path", + "required": true, + "description": "The ID of the return", + "schema": { + "type": "string", + "format": "uuid", + "description": "UUID of the return" + } + } + ], + "responses": { + "204": { + "description": "Successfully deleted the return" + }, + "401": { + "description": "See #/components/responses/401" + }, + "403": { + "description": "See #/components/responses/403" + }, + "404": { + "description": "See #/components/responses/404" + }, + "409": { + "description": "See #/components/responses/409" + }, + "422": { + "description": "See #/components/responses/422" + }, + "500": { + "description": "See #/components/responses/500" + }, + "502": { + "description": "See #/components/responses/502" + }, + "503": { + "description": "See #/components/responses/503" + }, + "504": { + "description": "See #/components/responses/504" + } + }, + "scopes": ["commerce.return:delete"] + }, + { + "operationId": "queryOrderReturns", + "method": "GET", + "path": "/v1/commerce/stores/{storeId}/orders/{orderId}/returns", + "summary": "Find returns for an order", + "description": "Find returns for an order", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "description": "Store ID", + "schema": { + "$ref": "./models/Order.yaml#/properties/context/properties/storeId" + } + }, + { + "name": "orderId", + "in": "path", + "required": true, + "description": "The ID of the order", + "schema": { + "$ref": "./models/Order.yaml#/properties/id" + } + } + ], + "responses": { + "200": { + "description": "Successfully listed returns", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Return" + } + } + }, + "401": { + "description": "See #/components/responses/401" + }, + "403": { + "description": "See #/components/responses/403" + }, + "404": { + "description": "See #/components/responses/404" + }, + "409": { + "description": "See #/components/responses/409" + }, + "422": { + "description": "See #/components/responses/422" + }, + "500": { + "description": "See #/components/responses/500" + }, + "502": { + "description": "See #/components/responses/502" + }, + "503": { + "description": "See #/components/responses/503" + }, + "504": { + "description": "See #/components/responses/504" + } + }, + "scopes": ["commerce.return:read"] + } + ] +} diff --git a/src/cli/schemas/api/registry.generated.ts b/src/cli/schemas/api/registry.generated.ts new file mode 100644 index 0000000..600fbc2 --- /dev/null +++ b/src/cli/schemas/api/registry.generated.ts @@ -0,0 +1,30 @@ +/** + * AUTO-GENERATED by scripts/generate-api-catalog.ts + * Do not edit manually. + */ + +import bulk_operations_json from "./bulk-operations.json"; +import businesses_json from "./businesses.json"; +import catalog_products_json from "./catalog-products.json"; +import channels_json from "./channels.json"; +import fulfillments_json from "./fulfillments.json"; +import location_addresses_json from "./location-addresses.json"; +import metafields_json from "./metafields.json"; +import onboarding_json from "./onboarding.json"; +import orders_json from "./orders.json"; +import stores_json from "./stores.json"; +import transactions_json from "./transactions.json"; + +export const DOMAIN_REGISTRY: Record = { + "bulk-operations": bulk_operations_json, + businesses: businesses_json, + "catalog-products": catalog_products_json, + channels: channels_json, + fulfillments: fulfillments_json, + "location-addresses": location_addresses_json, + metafields: metafields_json, + onboarding: onboarding_json, + orders: orders_json, + stores: stores_json, + transactions: transactions_json, +}; diff --git a/src/cli/schemas/api/stores.json b/src/cli/schemas/api/stores.json new file mode 100644 index 0000000..fa4d8db --- /dev/null +++ b/src/cli/schemas/api/stores.json @@ -0,0 +1,460 @@ +{ + "name": "stores", + "title": "Commerce Store API", + "description": "This API is capable of recording, updating and retrieving store information", + "version": "2.1.0", + "baseUrl": "https://api.godaddy.com/v2/commerce", + "endpoints": [ + { + "operationId": "createStore", + "method": "POST", + "path": "/stores", + "summary": "Create a new store", + "description": "Create a new store", + "parameters": [ + { + "name": "X-Request-Id", + "in": "header", + "required": false, + "description": "Request ID", + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "required": false, + "contentType": "application/json", + "schema": { + "$ref": "./models/Store.yaml" + } + }, + "responses": { + "200": { + "description": "Store Created", + "schema": { + "$ref": "./models/Store.yaml" + } + }, + "default": { + "description": "See #/components/responses/error" + } + }, + "scopes": ["commerce.store:create"] + }, + { + "operationId": "getStoresByBusinessId", + "method": "GET", + "path": "/stores", + "summary": "Get stores by business ID", + "description": "Retrieve the list of stores associated with a Business ID", + "parameters": [ + { + "name": "X-Request-Id", + "in": "header", + "required": false, + "description": "Request ID", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "businessId", + "in": "query", + "required": true, + "description": "Business ID", + "schema": { + "$ref": "./common-types/v1/uuid.json" + } + }, + { + "name": "If-Modified-Since", + "in": "header", + "required": false, + "description": "To check if data has been modified since a given date", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "page", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "default": 1 + } + }, + { + "name": "pageSize", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "default": 10 + } + }, + { + "name": "totalRequired", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "default": false + } + }, + { + "name": "includeFields", + "in": "query", + "required": false, + "description": "List of fields to include in the response", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + ], + "responses": { + "200": { + "description": "List of stores found", + "schema": { + "$ref": "./models/StoreList.yaml" + } + }, + "default": { + "description": "See #/components/responses/error" + } + }, + "scopes": ["commerce.store:create"] + }, + { + "operationId": "getStoreById", + "method": "GET", + "path": "/stores/{storeId}", + "summary": "Get store by ID", + "description": "Retrieve the information of a store using the Business and Store IDs.", + "parameters": [ + { + "name": "X-Request-Id", + "in": "header", + "required": false, + "description": "Request ID", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "storeId", + "in": "path", + "required": true, + "description": "Store ID", + "schema": { + "$ref": "./common-types/v1/uuid.json" + } + }, + { + "name": "includeFields", + "in": "query", + "required": false, + "description": "List of fields to include in the response", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + ], + "responses": { + "200": { + "description": "Store Found", + "schema": { + "$ref": "./models/Store.yaml" + } + }, + "default": { + "description": "See #/components/responses/error" + } + }, + "scopes": ["commerce.store:read"] + }, + { + "operationId": "updateStore", + "method": "PATCH", + "path": "/stores/{storeId}", + "summary": "Update store information", + "description": "Update the details of a store using the storeId.", + "parameters": [ + { + "name": "X-Request-Id", + "in": "header", + "required": false, + "description": "Request ID", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "storeId", + "in": "path", + "required": true, + "description": "Store ID", + "schema": { + "$ref": "./common-types/v1/uuid.json" + } + }, + { + "name": "includeFields", + "in": "query", + "required": false, + "description": "List of fields to include in the response", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + ], + "requestBody": { + "required": false, + "contentType": "application/json", + "schema": { + "$ref": "./common-types/v1/patch-request.json" + } + }, + "responses": { + "200": { + "description": "Store updated", + "schema": { + "$ref": "./models/Store.yaml" + } + }, + "404": { + "description": "Store Not Found" + }, + "default": { + "description": "See #/components/responses/error" + } + }, + "scopes": ["commerce.store:update"] + }, + { + "operationId": "updateGovernmentIds", + "method": "PUT", + "path": "/stores/{storeId}/government-ids", + "summary": "Update government IDs", + "description": "Update government IDs using the store ID.", + "parameters": [ + { + "name": "X-Request-Id", + "in": "header", + "required": false, + "description": "Request ID", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "storeId", + "in": "path", + "required": true, + "description": "Store ID", + "schema": { + "$ref": "./common-types/v1/uuid.json" + } + } + ], + "requestBody": { + "required": false, + "contentType": "application/json", + "schema": { + "$ref": "#/components/schemas/governmentIdList" + } + }, + "responses": { + "200": { + "description": "Government IDs updated", + "schema": { + "$ref": "./models/Store.yaml" + } + }, + "404": { + "description": "See #/components/responses/error" + } + }, + "scopes": ["commerce.store:update"] + }, + { + "operationId": "updateStoreAttribute", + "method": "PUT", + "path": "/stores/{storeId}/attributes/{attrName}", + "summary": "Update store attribute", + "description": "Modify the attribute of a store using the store ID and attribute name.", + "parameters": [ + { + "name": "X-Request-Id", + "in": "header", + "required": false, + "description": "Request ID", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "storeId", + "in": "path", + "required": true, + "description": "Store ID", + "schema": { + "$ref": "./common-types/v1/uuid.json" + } + }, + { + "name": "attrName", + "in": "path", + "required": true, + "description": "Attribute name", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": false, + "contentType": "application/json", + "schema": { + "$ref": "#/components/schemas/attributeUpdateRequest" + } + }, + "responses": { + "200": { + "description": "Store Attribute updated" + }, + "404": { + "description": "See #/components/responses/error" + }, + "default": { + "description": "See #/components/responses/error" + } + }, + "scopes": ["commerce.store:update"] + }, + { + "operationId": "getStoreAttribute", + "method": "GET", + "path": "/stores/{storeId}/attributes/{attrName}", + "summary": "Get store attribute", + "description": "Retrieve the information of a store attribute using the business ID, store ID and attribute name.", + "parameters": [ + { + "name": "X-Request-Id", + "in": "header", + "required": false, + "description": "Request ID", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "storeId", + "in": "path", + "required": true, + "description": "Store ID", + "schema": { + "$ref": "./common-types/v1/uuid.json" + } + }, + { + "name": "attrName", + "in": "path", + "required": true, + "description": "Attribute name", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Store Attribute Found" + }, + "404": { + "description": "See #/components/responses/error" + }, + "default": { + "description": "See #/components/responses/error" + } + }, + "scopes": ["commerce.store:read"] + }, + { + "operationId": "deleteStoreAttribute", + "method": "DELETE", + "path": "/stores/{storeId}/attributes/{attrName}", + "summary": "Delete store attribute", + "description": "Delete a store attribute using the store ID and attribute name.", + "parameters": [ + { + "name": "X-Request-Id", + "in": "header", + "required": false, + "description": "Request ID", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "storeId", + "in": "path", + "required": true, + "description": "Store ID", + "schema": { + "$ref": "./common-types/v1/uuid.json" + } + }, + { + "name": "attrName", + "in": "path", + "required": true, + "description": "Attribute name", + "schema": { + "type": "string" + } + }, + { + "name": "attrName", + "in": "path", + "required": true, + "description": "Attribute name", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Store Attribute deleted" + }, + "404": { + "description": "See #/components/responses/error" + }, + "default": { + "description": "See #/components/responses/error" + } + }, + "scopes": ["commerce.store:update"] + } + ] +} diff --git a/src/cli/schemas/api/transactions.json b/src/cli/schemas/api/transactions.json new file mode 100644 index 0000000..bf073a6 --- /dev/null +++ b/src/cli/schemas/api/transactions.json @@ -0,0 +1,442 @@ +{ + "name": "transactions", + "title": "Transactions API", + "description": "This API is capable of creating, updating and fetching transactions across different stores.", + "version": "2.1.0", + "baseUrl": "https://api.godaddy.com/v2/commerce", + "endpoints": [ + { + "operationId": "getTransactions", + "method": "GET", + "path": "/stores/{storeId}/transactions", + "summary": "Get all transactions", + "description": "Retrieve all the transactions for a specific store using the store ID.", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "schema": { + "$ref": "./types/Uuid.yaml" + } + }, + { + "name": "Request Id", + "in": "header", + "required": false, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "page", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "default": 1 + } + }, + { + "name": "pageSize", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "default": 10 + } + }, + { + "name": "totalRequired", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "default": false + } + }, + { + "name": "transactionIds", + "in": "query", + "required": false, + "description": "Transaction ids to filter", + "schema": { + "type": "array", + "items": { + "$ref": "./types/Id.yaml" + } + } + }, + { + "name": "updatedAtAfter", + "in": "query", + "required": false, + "description": "Transaction filter updatedAt start date", + "schema": { + "$ref": "./common-types/date-time.json" + } + }, + { + "name": "updatedAtBefore", + "in": "query", + "required": false, + "description": "Transaction filter updatedAt end date", + "schema": { + "$ref": "./common-types/date-time.json" + } + }, + { + "name": "sortBy", + "in": "query", + "required": false, + "description": "Transaction sort field", + "schema": { + "type": "string", + "enum": ["updatedAt"], + "default": "updatedAt" + } + }, + { + "name": "sortOrder", + "in": "query", + "required": false, + "description": "Transaction sort order", + "schema": { + "type": "string", + "enum": ["ASC", "DESC"], + "default": "ASC" + } + } + ], + "responses": { + "200": { + "description": "Transactions List", + "schema": { + "title": "Transactions", + "allOf": [ + { + "$ref": "#/components/schemas/pagedList" + } + ], + "properties": { + "items": { + "items": { + "$ref": "./models/transaction/Transaction.yaml" + }, + "type": "array" + } + } + } + }, + "default": { + "description": "See #/components/responses/error" + } + }, + "scopes": ["commerce.transaction:read", "commerce.transaction:write"] + }, + { + "operationId": "getTransactionById", + "method": "GET", + "path": "/stores/{storeId}/transactions/{transactionId}", + "summary": "Get transaction by ID", + "description": "Retrieve all the information for a single transaction using the store and transaction IDs.", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "schema": { + "$ref": "./types/Uuid.yaml" + } + }, + { + "name": "transactionId", + "in": "path", + "required": true, + "schema": { + "$ref": "./types/Id.yaml" + } + }, + { + "name": "Request Id", + "in": "header", + "required": false, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Transaction Found", + "schema": { + "$ref": "./models/transaction/Transaction.yaml" + } + }, + "default": { + "description": "See #/components/responses/error" + } + }, + "scopes": ["commerce.transaction:read", "commerce.transaction:write"] + }, + { + "operationId": "addTransactionSignatureByTransactionId", + "method": "POST", + "path": "/stores/{storeId}/transactions/{transactionId}/signatures", + "summary": "Add transaction signature", + "description": "Add a transaction signature using the store and transaction IDs.", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "schema": { + "$ref": "./types/Uuid.yaml" + } + }, + { + "name": "transactionId", + "in": "path", + "required": true, + "schema": { + "$ref": "./types/Id.yaml" + } + }, + { + "name": "Request Id", + "in": "header", + "required": false, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "required": false, + "contentType": "multipart/form-data", + "schema": { + "$ref": "./models/transaction/Signature.yaml" + } + }, + "responses": { + "200": { + "description": "See #/components/responses/200" + }, + "default": { + "description": "See #/components/responses/error" + } + }, + "scopes": ["commerce.transaction:write"] + }, + { + "operationId": "getTransactionSignaturesById", + "method": "GET", + "path": "/stores/{storeId}/transactions/{transactionId}/signatures", + "summary": "Get transaction signatures", + "description": "Get all transaction signatures using the store and transaction IDs.", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "schema": { + "$ref": "./types/Uuid.yaml" + } + }, + { + "name": "transactionId", + "in": "path", + "required": true, + "schema": { + "$ref": "./types/Id.yaml" + } + }, + { + "name": "Request Id", + "in": "header", + "required": false, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "page", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "default": 1 + } + }, + { + "name": "pageSize", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "default": 10 + } + }, + { + "name": "totalRequired", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "default": false + } + } + ], + "responses": { + "200": { + "description": "Transaction Signature list", + "schema": { + "title": "Transaction Signatures", + "$ref": "#/components/schemas/signatureList" + } + }, + "default": { + "description": "See #/components/responses/error" + } + }, + "scopes": ["commerce.transaction:read", "commerce.transaction:write"] + }, + { + "operationId": "getTransactionReference", + "method": "GET", + "path": "/stores/{storeId}/transactions/{transactionId}/references/{value}", + "summary": "Get transaction reference", + "description": "Retrieve a transaction reference using the store ID, transaction ID and reference value.", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "schema": { + "$ref": "./types/Uuid.yaml" + } + }, + { + "name": "transactionId", + "in": "path", + "required": true, + "schema": { + "$ref": "./types/Id.yaml" + } + }, + { + "name": "value", + "in": "path", + "required": true, + "description": "Reference value", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Transaction references by value", + "schema": { + "$ref": "./models/TransactionReference.yaml" + } + } + }, + "scopes": ["commerce.transaction:read", "commerce.transaction:write"] + }, + { + "operationId": "createTransactionReference", + "method": "PUT", + "path": "/stores/{storeId}/transactions/{transactionId}/references/{value}", + "summary": "Create transaction reference", + "description": "Create a new transaction reference using the store ID, transaction ID and reference value.", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "schema": { + "$ref": "./types/Uuid.yaml" + } + }, + { + "name": "transactionId", + "in": "path", + "required": true, + "schema": { + "$ref": "./types/Id.yaml" + } + }, + { + "name": "value", + "in": "path", + "required": true, + "description": "Reference value", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": false, + "contentType": "application/json", + "schema": { + "$ref": "./models/TransactionReference.yaml" + } + }, + "responses": { + "204": { + "description": "Resource created / updated successfully" + } + }, + "scopes": ["commerce.transaction:write"] + }, + { + "operationId": "deleteTransactionReference", + "method": "DELETE", + "path": "/stores/{storeId}/transactions/{transactionId}/references/{value}", + "summary": "Delete reference", + "description": "Delete a transaction reference using the store ID, transaction ID and reference value.", + "parameters": [ + { + "name": "storeId", + "in": "path", + "required": true, + "schema": { + "$ref": "./types/Uuid.yaml" + } + }, + { + "name": "transactionId", + "in": "path", + "required": true, + "schema": { + "$ref": "./types/Id.yaml" + } + }, + { + "name": "value", + "in": "path", + "required": true, + "description": "Reference value", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "See #/components/responses/200" + }, + "404": { + "description": "Reference Not Found" + } + }, + "scopes": ["commerce.transaction:write"] + } + ] +} diff --git a/src/cli/services/envelope-writer.ts b/src/cli/services/envelope-writer.ts index a905279..fa165c7 100644 --- a/src/cli/services/envelope-writer.ts +++ b/src/cli/services/envelope-writer.ts @@ -30,7 +30,7 @@ export interface EnvelopeWriterShape { */ readonly emitError: ( command: string, - error: { message: string; code: string }, + error: { message: string; code: string; details?: Record }, fix: string, nextActions: NextAction[], ) => Effect.Effect; @@ -55,7 +55,7 @@ export interface EnvelopeWriterShape { */ readonly emitStreamError: ( command: string, - error: { message: string; code: string }, + error: { message: string; code: string; details?: Record }, fix: string, nextActions: NextAction[], ) => Effect.Effect; @@ -114,7 +114,11 @@ export const EnvelopeWriterLive: Layer.Layer = const emitError = ( command: string, - error: { message: string; code: string }, + error: { + message: string; + code: string; + details?: Record; + }, fix: string, nextActions: NextAction[], ): Effect.Effect => @@ -160,7 +164,11 @@ export const EnvelopeWriterLive: Layer.Layer = const emitStreamError = ( command: string, - error: { message: string; code: string }, + error: { + message: string; + code: string; + details?: Record; + }, fix: string, nextActions: NextAction[], ): Effect.Effect => @@ -234,7 +242,11 @@ export const makeTestEnvelopeWriter = (): Effect.Effect< emitError: ( command: string, - error: { message: string; code: string }, + error: { + message: string; + code: string; + details?: Record; + }, fix: string, nextActions: NextAction[], ) => @@ -271,7 +283,11 @@ export const makeTestEnvelopeWriter = (): Effect.Effect< emitStreamError: ( command: string, - error: { message: string; code: string }, + error: { + message: string; + code: string; + details?: Record; + }, fix: string, nextActions: NextAction[], ) => diff --git a/src/core/api.ts b/src/core/api.ts index 73efad9..7c46c46 100644 --- a/src/core/api.ts +++ b/src/core/api.ts @@ -71,6 +71,193 @@ function redactSensitiveBodyFields(body: string): string { } } +const MAX_ERROR_BODY_CHARS = 4000; +const MAX_ERROR_SUMMARY_CHARS = 240; +const MAX_ERROR_DEPTH = 6; +const MAX_ERROR_ARRAY_ITEMS = 40; +const MAX_ERROR_OBJECT_KEYS = 80; +const DEFAULT_USER_AGENT = "godaddy-cli"; + +function findHeaderKey( + headers: Record, + headerName: string, +): string | undefined { + const target = headerName.toLowerCase(); + return Object.keys(headers).find((key) => key.toLowerCase() === target); +} + +function getHeaderValue( + headers: Record, + headerName: string, +): string | undefined { + const key = findHeaderKey(headers, headerName); + return key ? headers[key] : undefined; +} + +function hasNonEmptyHeader( + headers: Record, + headerName: string, +): boolean { + const value = getHeaderValue(headers, headerName); + return typeof value === "string" && value.trim().length > 0; +} + +function ensureRequiredRequestHeaders(headers: Record): void { + if (!hasNonEmptyHeader(headers, "x-request-id")) { + headers["x-request-id"] = uuid(); + } + + if (!hasNonEmptyHeader(headers, "user-agent")) { + headers["user-agent"] = DEFAULT_USER_AGENT; + } +} + +function truncateString(value: string, maxChars: number): string { + if (value.length <= maxChars) return value; + return `${value.slice(0, maxChars)}…`; +} + +function sanitizeErrorValue(value: unknown, depth = 0): unknown { + if (depth > MAX_ERROR_DEPTH) { + return "[TRUNCATED_DEPTH]"; + } + + if (typeof value === "string") { + return truncateString(value, MAX_ERROR_BODY_CHARS); + } + + if ( + typeof value === "number" || + typeof value === "boolean" || + value === null || + value === undefined + ) { + return value; + } + + if (Array.isArray(value)) { + const limited = value + .slice(0, MAX_ERROR_ARRAY_ITEMS) + .map((item) => sanitizeErrorValue(item, depth + 1)); + + if (value.length > MAX_ERROR_ARRAY_ITEMS) { + limited.push({ + truncated: true, + omitted_items: value.length - MAX_ERROR_ARRAY_ITEMS, + }); + } + + return limited; + } + + if (typeof value === "object") { + const sanitized: Record = {}; + const entries = Object.entries(value); + const limitedEntries = entries.slice(0, MAX_ERROR_OBJECT_KEYS); + + for (const [key, entry] of limitedEntries) { + sanitized[key] = isSensitiveHeader(key) + ? "[REDACTED]" + : sanitizeErrorValue(entry, depth + 1); + } + + if (entries.length > MAX_ERROR_OBJECT_KEYS) { + sanitized.__truncated_keys__ = entries.length - MAX_ERROR_OBJECT_KEYS; + } + + return sanitized; + } + + return String(value); +} + +function summarizeApiErrorBody(value: unknown): string | undefined { + if (typeof value === "string") { + const trimmed = value.trim(); + if (!trimmed) return undefined; + return truncateString(trimmed, MAX_ERROR_SUMMARY_CHARS); + } + + if (typeof value !== "object" || value === null) { + return undefined; + } + + const record = value as Record; + const candidates = [ + record.message, + record.error, + record.detail, + record.title, + record.code, + ]; + + for (const candidate of candidates) { + if (typeof candidate === "string" && candidate.trim().length > 0) { + return truncateString(candidate.trim(), MAX_ERROR_SUMMARY_CHARS); + } + } + + const fields = record.fields; + if (Array.isArray(fields) && fields.length > 0) { + const first = fields[0]; + if (typeof first === "object" && first !== null) { + const firstRecord = first as Record; + const fieldPath = + typeof firstRecord.path === "string" ? firstRecord.path : "field"; + const fieldMessage = + typeof firstRecord.message === "string" + ? firstRecord.message + : "validation failed"; + return truncateString( + `${fieldPath}: ${fieldMessage}`, + MAX_ERROR_SUMMARY_CHARS, + ); + } + } + + return undefined; +} + +function extractGraphqlErrors(value: unknown): unknown[] | undefined { + if (typeof value !== "object" || value === null) { + return undefined; + } + + const errors = (value as Record).errors; + if (!Array.isArray(errors) || errors.length === 0) { + return undefined; + } + + return errors; +} + +function summarizeGraphqlErrors(errors: unknown[]): string | undefined { + for (const entry of errors) { + if (typeof entry === "string" && entry.trim().length > 0) { + return truncateString(entry.trim(), MAX_ERROR_SUMMARY_CHARS); + } + + if (typeof entry === "object" && entry !== null) { + const message = (entry as Record).message; + if (typeof message === "string" && message.trim().length > 0) { + return truncateString(message.trim(), MAX_ERROR_SUMMARY_CHARS); + } + } + } + + return undefined; +} + +function responseRequestId( + headers: Record, +): string | undefined { + return ( + headers["godaddy-request-id"] || + headers["x-request-id"] || + headers["x-amzn-requestid"] + ); +} + export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE"; export interface ApiRequestOptions { @@ -80,6 +267,7 @@ export interface ApiRequestOptions { body?: string; headers?: Record; debug?: boolean; + graphql?: boolean; } export interface ApiResponse { @@ -259,6 +447,7 @@ export function apiRequestEffect( body, headers = {}, debug, + graphql = false, } = options; // Get access token with expiry info @@ -300,21 +489,21 @@ export function apiRequestEffect( // Build headers const requestHeaders: Record = { Authorization: `Bearer ${accessToken}`, - "X-Request-ID": uuid(), ...headers, }; + ensureRequiredRequestHeaders(requestHeaders); // Build body let requestBody: string | undefined; if (body) { requestBody = body; - if (!requestHeaders["Content-Type"]) { - requestHeaders["Content-Type"] = "application/json"; + if (!hasNonEmptyHeader(requestHeaders, "content-type")) { + requestHeaders["content-type"] = "application/json"; } } else if (fields && Object.keys(fields).length > 0) { requestBody = JSON.stringify(fields); - if (!requestHeaders["Content-Type"]) { - requestHeaders["Content-Type"] = "application/json"; + if (!hasNonEmptyHeader(requestHeaders, "content-type")) { + requestHeaders["content-type"] = "application/json"; } } @@ -393,14 +582,52 @@ export function apiRequestEffect( }); } + if (response.ok && graphql) { + const graphqlErrors = extractGraphqlErrors(data); + if (graphqlErrors && graphqlErrors.length > 0) { + const safeErrorBody = sanitizeErrorValue(data); + const summary = summarizeGraphqlErrors(graphqlErrors); + const requestId = responseRequestId(responseHeaders); + const internalDetail = + typeof safeErrorBody === "string" + ? safeErrorBody + : JSON.stringify(safeErrorBody); + + return yield* Effect.fail( + new NetworkError({ + message: `GraphQL API error(s): ${internalDetail}`, + userMessage: summary + ? `GraphQL request returned errors: ${summary}` + : `GraphQL request returned ${graphqlErrors.length} error(s).`, + status: response.status, + statusText: response.statusText, + endpoint, + method, + requestId, + responseBody: safeErrorBody, + }), + ); + } + } + // Check for error status codes if (!response.ok) { - // Internal message includes the raw server payload for debugging; - // userMessage is a safe, generic description shown to users/agents. + const safeErrorBody = sanitizeErrorValue(data); + const summary = summarizeApiErrorBody(safeErrorBody); + const requestId = responseRequestId(responseHeaders); const internalDetail = - typeof data === "object" && data !== null - ? JSON.stringify(data) - : String(data || response.statusText); + typeof safeErrorBody === "string" + ? safeErrorBody + : JSON.stringify(safeErrorBody); + + const context = { + status: response.status, + statusText: response.statusText, + endpoint, + method, + requestId, + responseBody: safeErrorBody, + }; // Handle 401 Unauthorized specifically - token may be revoked or invalid if (response.status === 401) { @@ -409,6 +636,7 @@ export function apiRequestEffect( message: `Authentication failed (401): ${internalDetail}`, userMessage: "Your session has expired or is invalid. Run 'godaddy auth login' to re-authenticate.", + ...context, }), ); } @@ -420,14 +648,23 @@ export function apiRequestEffect( message: `Access denied (403): ${internalDetail}`, userMessage: "You don't have permission to access this resource. Check your account permissions.", + ...context, }), ); } + const userMessage = + response.status >= 400 && response.status < 500 + ? summary + ? `API request rejected (${response.status}): ${summary}` + : `API request rejected with status ${response.status}: ${response.statusText}` + : `API request failed with status ${response.status}: ${response.statusText}`; + return yield* Effect.fail( new NetworkError({ message: `API error (${response.status}): ${internalDetail}`, - userMessage: `API request failed with status ${response.status}: ${response.statusText}`, + userMessage, + ...context, }), ); } diff --git a/src/effect/errors.ts b/src/effect/errors.ts index e478b2a..2f4ea0f 100644 --- a/src/effect/errors.ts +++ b/src/effect/errors.ts @@ -5,17 +5,30 @@ export class ValidationError extends Data.TaggedError("ValidationError")<{ readonly userMessage: string; }> {} -export class NetworkError extends Data.TaggedError("NetworkError")<{ - readonly message: string; - readonly userMessage: string; -}> {} +export interface ApiErrorContext { + readonly status?: number; + readonly statusText?: string; + readonly endpoint?: string; + readonly method?: string; + readonly requestId?: string; + readonly responseBody?: unknown; +} + +export class NetworkError extends Data.TaggedError("NetworkError")< + { + readonly message: string; + readonly userMessage: string; + } & ApiErrorContext +> {} export class AuthenticationError extends Data.TaggedError( "AuthenticationError", -)<{ - readonly message: string; - readonly userMessage: string; -}> {} +)< + { + readonly message: string; + readonly userMessage: string; + } & ApiErrorContext +> {} export class ConfigurationError extends Data.TaggedError("ConfigurationError")<{ readonly message: string; diff --git a/tests/integration/cli-smoke.test.ts b/tests/integration/cli-smoke.test.ts index 339e6b7..2bc3550 100644 --- a/tests/integration/cli-smoke.test.ts +++ b/tests/integration/cli-smoke.test.ts @@ -98,18 +98,32 @@ describe("CLI Smoke Tests", () => { }; expect(payload.ok).toBe(true); expect(payload.command).toBe("godaddy api list"); - expect( - payload.result.domains.some( - (domain) => domain.name === "location-addresses", - ), - ).toBe(true); + const expectedDomains = [ + "location-addresses", + "catalog-products", + "orders", + "stores", + "fulfillments", + "metafields", + "transactions", + "businesses", + "bulk-operations", + "channels", + "onboarding", + ]; + + for (const expectedDomain of expectedDomains) { + expect( + payload.result.domains.some((domain) => domain.name === expectedDomain), + ).toBe(true); + } }); it("api describe returns endpoint details", () => { const result = runCli([ "api", "describe", - "commerce.location.verify-address", + "/location/address-verifications", ]); expect(result.status).toBe(0); @@ -135,6 +149,27 @@ describe("CLI Smoke Tests", () => { ); }); + it("api describe matches templated catalog paths", () => { + const result = runCli([ + "api", + "describe", + "/stores/123e4567-e89b-12d3-a456-426614174000/catalog-subgraph", + ]); + expect(result.status).toBe(0); + + const payload = JSON.parse(result.stdout) as { + ok: boolean; + command: string; + result: { operationId: string; method: string; path: string }; + }; + + expect(payload.ok).toBe(true); + expect(payload.command).toBe("godaddy api describe"); + expect(payload.result.operationId).toBe("postCatalogGraphql"); + expect(payload.result.method).toBe("POST"); + expect(payload.result.path).toBe("/stores/{storeId}/catalog-subgraph"); + }); + it("api search returns matching endpoints", () => { const result = runCli(["api", "search", "address"]); expect(result.status).toBe(0); @@ -169,6 +204,21 @@ describe("CLI Smoke Tests", () => { expect(result.stdout).toContain(""); }); + it("api call rejects untrusted absolute URLs", () => { + const result = runCli(["api", "call", "https://example.com/v1/domains"]); + expect(result.status).toBe(1); + + const payload = JSON.parse(result.stdout) as { + ok: boolean; + error: { code: string; message: string }; + fix: string; + }; + + expect(payload.ok).toBe(false); + expect(payload.error.code).toBe("VALIDATION_ERROR"); + expect(payload.error.message).toContain("trusted GoDaddy API URL"); + }); + it("unknown command returns structured error envelope", () => { const result = runCli(["nonexistent-command"]); expect(result.status).toBe(1); diff --git a/tests/unit/cli/api-catalog.test.ts b/tests/unit/cli/api-catalog.test.ts new file mode 100644 index 0000000..e49ce37 --- /dev/null +++ b/tests/unit/cli/api-catalog.test.ts @@ -0,0 +1,24 @@ +import * as Option from "effect/Option"; +import { describe, expect, test } from "vitest"; +import { findEndpointByPathEffect } from "../../../src/cli/schemas/api"; +import { runEffect } from "../../setup/effect-test-utils"; + +describe("API catalog path resolution", () => { + test("matches parameterized catalog path against concrete request path", async () => { + const result = await runEffect( + findEndpointByPathEffect( + "POST", + "/stores/123e4567-e89b-12d3-a456-426614174000/catalog-subgraph", + ), + ); + + expect(Option.isSome(result)).toBe(true); + if (Option.isSome(result)) { + expect(result.value.domain.name).toBe("catalog-products"); + expect(result.value.endpoint.path).toBe( + "/stores/{storeId}/catalog-subgraph", + ); + expect(result.value.endpoint.method).toBe("POST"); + } + }); +}); diff --git a/tests/unit/core/api.test.ts b/tests/unit/core/api.test.ts index 10420b5..5bf7595 100644 --- a/tests/unit/core/api.test.ts +++ b/tests/unit/core/api.test.ts @@ -81,7 +81,8 @@ describe("API Core Functions", () => { method: "GET", headers: expect.objectContaining({ Authorization: "Bearer test-token-123", - "X-Request-ID": expect.any(String), + "x-request-id": expect.any(String), + "user-agent": "godaddy-cli", }), }), ); @@ -105,6 +106,111 @@ describe("API Core Functions", () => { expect(err._tag).toBe("AuthenticationError"); expect(err.userMessage).toContain("re-authenticate"); }); + + test("returns network error when graphql response contains errors", async () => { + vi.mocked(fetch).mockResolvedValueOnce( + new Response( + JSON.stringify({ + data: { sku: null }, + errors: [{ message: "Cannot query field 'skuX' on type 'Query'." }], + }), + { + status: 200, + statusText: "OK", + headers: { + "content-type": "application/json", + "x-request-id": "graphql-req-1", + }, + }, + ), + ); + + const exit = await runEffectExit( + apiRequestEffect({ + endpoint: "/v2/commerce/stores/test/catalog/graphql", + method: "POST", + body: JSON.stringify({ query: "{ skuX { id } }" }), + graphql: true, + }), + ); + + const err = extractFailure(exit) as { + _tag: string; + userMessage: string; + status?: number; + requestId?: string; + responseBody?: unknown; + }; + + expect(err._tag).toBe("NetworkError"); + expect(err.userMessage).toContain("GraphQL request returned errors"); + expect(err.userMessage).toContain("Cannot query field"); + expect(err.status).toBe(200); + expect(err.requestId).toBe("graphql-req-1"); + expect(err.responseBody).toEqual( + expect.objectContaining({ + errors: expect.arrayContaining([ + expect.objectContaining({ + message: "Cannot query field 'skuX' on type 'Query'.", + }), + ]), + }), + ); + }); + + test("returns structured network error details for 400 responses", async () => { + vi.mocked(fetch).mockResolvedValueOnce( + new Response( + JSON.stringify({ + code: "VALIDATION_FAILED", + message: "Validation error", + fields: [{ path: "lineItems[0].sku", message: "Required" }], + }), + { + status: 400, + statusText: "Bad Request", + headers: { + "content-type": "application/json", + "godaddy-request-id": "req-123", + }, + }, + ), + ); + + const exit = await runEffectExit( + apiRequestEffect({ + endpoint: "/v1/commerce/stores/test-store/orders", + method: "POST", + body: "{}", + }), + ); + + const err = extractFailure(exit) as { + _tag: string; + userMessage: string; + status?: number; + statusText?: string; + endpoint?: string; + method?: string; + requestId?: string; + responseBody?: unknown; + }; + + expect(err._tag).toBe("NetworkError"); + expect(err.userMessage).toContain("API request rejected (400)"); + expect(err.userMessage).toContain("Validation error"); + expect(err.status).toBe(400); + expect(err.statusText).toBe("Bad Request"); + expect(err.endpoint).toBe("/v1/commerce/stores/test-store/orders"); + expect(err.method).toBe("POST"); + expect(err.requestId).toBe("req-123"); + expect(err.responseBody).toEqual( + expect.objectContaining({ + code: "VALIDATION_FAILED", + message: "Validation error", + }), + ); + }); }); describe("parseFieldsEffect", () => {