diff --git a/package-lock.json b/package-lock.json index de97614..543fb8e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { "name": "vscode-apimanagement", - "version": "1.0.2-beta", + "version": "1.0.3-beta", "lockfileVersion": 1, "requires": true, "dependencies": { "@azure/abort-controller": { - "version": "1.0.3", + "version": "1.0.2", "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.0.2.tgz", "integrity": "sha512-XUyTo+bcyxHEf+jlN2MXA7YU9nxVehaubngHV1MIZZaqYmZqykkoeAz/JMMEeR7t3TcyDwbFa3Zw8BZywmIx4g==", "requires": { @@ -4412,6 +4412,11 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==" }, + "graphql": { + "version": "15.5.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-15.5.0.tgz", + "integrity": "sha512-OmaM7y0kaK31NKG31q4YbD2beNYa6jBBKtMFT6gLYJljHLJr42IqJ8KX08u3Li/0ifzTU5HjmoOOrwa5BRLeDA==" + }, "growl": { "version": "1.10.5", "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", diff --git a/package.json b/package.json index 6ee1a00..cd48d7d 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "vscode-apimanagement", "displayName": "Azure API Management", "description": "An Azure API Management extension for Visual Studio Code.", - "version": "1.0.3", + "version": "1.0.3-beta", "publisher": "ms-azuretools", "icon": "resources/apim-icon-newone.png", "aiKey": "AIF-d9b70cd4-b9f9-4d70-929b-a071c400b217", @@ -40,15 +40,20 @@ "onCommand:azureApiManagement.deleteOperation", "onCommand:azureApiManagement.importOpenApiByFile", "onCommand:azureApiManagement.importOpenApiByLink", + "onCommand:azureApiManagement.importGraphqlAPIByLink", "onCommand:azureApiManagement.testOperation", + "onCommand:azureApiManagement.importGraphqlSchemaByFile", "onCommand:azureApiManagement.openInPortal", "onCommand:azureApiManagement.showApi", "onCommand:azureApiManagement.showArmApi", + "onCommand:azureApiManagement.showArmGraphqlApi", + "onCommand:azureApiManagement.showGraphqlAPIQuery", "onCommand:azureApiManagement.showArmApiOperation", "onCommand:azureApiManagement.showArmProduct", "onCommand:azureApiManagement.showServicePolicy", "onCommand:azureApiManagement.showApiPolicy", "onCommand:azureApiManagement.showOperationPolicy", + "onCommand:azureApiManagement.showGraphqlSchema", "onCommand:azureApiManagement.createNamedValue", "onCommand:azureApiManagement.deleteNamedValue", "onCommand:azureApiManagement.updateNamedValue", @@ -169,11 +174,26 @@ "title": "%azureApiManagement.importOpenApiByLink%", "category": "Azure API Management" }, + { + "command": "azureApiManagement.importGraphqlAPIByLink", + "title": "%azureApiManagement.importGraphqlAPIByLink%", + "category": "Azure API Management" + }, { "command": "azureApiManagement.testOperation", "title": "%azureApiManagement.testOperation%", "category": "Azure API Management" }, + { + "command": "azureApiManagement.importGraphqlSchemaByFile", + "title": "%azureApiManagement.importGraphqlSchemaByFile%", + "category": "Azure API Management" + }, + { + "command": "azureApiManagement.showGraphqlSchema", + "title": "%azureApiManagement.showGraphqlSchema%", + "category": "Azure API Management" + }, { "command": "azureApiManagement.openInPortal", "title": "%azureApiManagement.openInPortal%", @@ -215,6 +235,16 @@ "title": "%azureApiManagement.showArmApi%", "category": "Azure API Management" }, + { + "command": "azureApiManagement.showGraphqlAPIQuery", + "title": "%azureApiManagement.showGraphqlAPIQuery%", + "category": "Azure API Management" + }, + { + "command": "azureApiManagement.showArmGraphqlApi", + "title": "%azureApiManagement.showArmGraphqlApi%", + "category": "Azure API Management" + }, { "command": "azureApiManagement.showArmApiOperation", "title": "%azureApiManagement.showArmApiOperation%", @@ -491,6 +521,11 @@ "when": "view == azureApiManagementExplorer && viewItem == azureApiManagementApis", "group": "1@2" }, + { + "command": "azureApiManagement.importGraphqlAPIByLink", + "when": "view == azureApiManagementExplorer && viewItem == azureApiManagementApis", + "group": "1@3" + }, { "command": "azureApiManagement.importFunctionApp", "when": "view == azureApiManagementExplorer && viewItem == azureApiManagementApis", @@ -581,6 +616,21 @@ "when": "view == azureApiManagementExplorer && viewItem == azureApiManagementNamedValues", "group": "1@1" }, + { + "command": "azureApiManagement.showGraphqlAPIQuery", + "when": "view == azureApiManagementExplorer && viewItem == azureApiManagementGraphqlObjectType", + "group": "1@1" + }, + { + "command": "azureApiManagement.importGraphqlSchemaByFile", + "when": "view == azureApiManagementExplorer && viewItem == azureApiManagementGraphql", + "group": "1@1" + }, + { + "command": "azureApiManagement.showGraphqlSchema", + "when": "view == azureApiManagementExplorer && viewItem == azureApiManagementGraphql", + "group": "1@2" + }, { "command": "azureApiManagement.deleteNamedValue", "when": "view == azureApiManagementExplorer && viewItem == azureApiManagementNamedValue", @@ -723,6 +773,7 @@ "await-notify": "1.0.1", "decompress": "^4.2.1", "fs-extra": "^4.0.2", + "graphql": "^15.5.0", "guid-typescript": "^1.0.9", "gulp-decompress": "^3.0.0", "gulp-download": "0.0.1", diff --git a/package.nls.json b/package.nls.json index a623e6a..8b18f9a 100644 --- a/package.nls.json +++ b/package.nls.json @@ -2,6 +2,7 @@ "azureApiManagement.openInPortal": "Open in Portal", "azureApiManagement.importOpenApiByFile": "Import from OpenAPI file", "azureApiManagement.importOpenApiByLink": "Import from OpenAPI link", + "azureApiManagement.importGraphqlAPIByLink": "Import Graphql API from OpenAPI link", "azureApiManagement.showExplorer": "Show or hide the Azure API Management Explorer.", "azureApiManagement.showSavePrompt": "Show warning dialog on remote file uploading.", "azureApiManagement.createService": "Create API Management in Azure", @@ -9,13 +10,17 @@ "azureApiManagement.copySubscriptionKey" : "Copy Subscription Key", "azureApiManagement.showApi": "Edit OpenAPI", "azureApiManagement.showArmApi": "Edit API", + "azureApiManagement.showArmGraphqlApi": "Edit Graphql API", "azureApiManagement.showArmApiOperation": "Edit Operation", + "azureApiManagement.showGraphqlAPIQuery": "Test Grapql Query", "azureApiManagement.showServicePolicy": "Edit Global policy", "azureApiManagement.showApiPolicy": "Edit API policy", "azureApiManagement.showOperationPolicy": "Edit Operation policy", "azureApiManagement.deleteApi": "Delete API", "azureApiManagement.deleteOperation": "Delete Operation", "azureApiManagement.testOperation": "Test Operation", + "azureApiManagement.importGraphqlSchemaByFile": "Import Graphql API schema file", + "azureApiManagement.showGraphqlSchema": "Open Schema", "azureApiManagement.advancedCreationDescription":"Enables advanced creation of Azure API Management Instance, which will prompt for several additional values instead of using a default.", "azureApiManagement.createNamedValue": "Create Named value", "azureApiManagement.deleteNamedValue": "Delete Named value", diff --git a/resources/dark/leafnode.svg b/resources/dark/leafnode.svg new file mode 100644 index 0000000..e2971d4 --- /dev/null +++ b/resources/dark/leafnode.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/resources/dark/mutation.svg b/resources/dark/mutation.svg new file mode 100644 index 0000000..95d8beb --- /dev/null +++ b/resources/dark/mutation.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/resources/dark/querylist.svg b/resources/dark/querylist.svg new file mode 100644 index 0000000..748c55c --- /dev/null +++ b/resources/dark/querylist.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/resources/dark/treelist.svg b/resources/dark/treelist.svg new file mode 100644 index 0000000..dcf7226 --- /dev/null +++ b/resources/dark/treelist.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/resources/light/leafnode.svg b/resources/light/leafnode.svg new file mode 100644 index 0000000..c339a56 --- /dev/null +++ b/resources/light/leafnode.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/resources/light/mutation.svg b/resources/light/mutation.svg new file mode 100644 index 0000000..ce678aa --- /dev/null +++ b/resources/light/mutation.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/resources/light/querylist.svg b/resources/light/querylist.svg new file mode 100644 index 0000000..77ad82c --- /dev/null +++ b/resources/light/querylist.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/resources/light/treelist.svg b/resources/light/treelist.svg new file mode 100644 index 0000000..a461b02 --- /dev/null +++ b/resources/light/treelist.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/azure/apim/TempApiContract.ts b/src/azure/apim/TempApiContract.ts new file mode 100644 index 0000000..35fa51c --- /dev/null +++ b/src/azure/apim/TempApiContract.ts @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AuthenticationSettingsContract, Protocol, SubscriptionKeyParameterNamesContract } from "@azure/arm-apimanagement/src/models"; + +export interface IApiContract { + id: string; + name: string; + properties: { + displayName: string; + apiRevision: string; + authenticationSettings?: AuthenticationSettingsContract; + description: string; + isCurrent: boolean; + path: string; + protocols?: Protocol[]; + serviceUrl: string; + subscriptionKeyParameterNames?: SubscriptionKeyParameterNamesContract; + subscriptionRequired?: boolean; + // tslint:disable-next-line: no-reserved-keywords + type: string; + }; +} diff --git a/src/azure/apim/TempSchema.ts b/src/azure/apim/TempSchema.ts new file mode 100644 index 0000000..94472b8 --- /dev/null +++ b/src/azure/apim/TempSchema.ts @@ -0,0 +1,18 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export class TempSchema { + public id: string; + public name: string; + // tslint:disable-next-line: no-reserved-keywords + public type: string; + public location?: string; + public properties: { + contentType: string, + document: { + value: string + } + }; +} diff --git a/src/commands/createGraphqlApi.ts b/src/commands/createGraphqlApi.ts new file mode 100644 index 0000000..0d7e3b6 --- /dev/null +++ b/src/commands/createGraphqlApi.ts @@ -0,0 +1,85 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +//import { ServiceClient } from "@azure/ms-rest-js"; +import { ServiceClient } from "@azure/ms-rest-js"; +import { ProgressLocation, window } from "vscode"; +import { createGenericClient } from 'vscode-azureextensionui'; +import { IActionContext } from "../../extension.bundle"; +import { ApisTreeItem, IApiTreeItemContext } from "../explorer/ApisTreeItem"; +import { ServiceTreeItem } from "../explorer/ServiceTreeItem"; +import { ext } from "../extensionVariables"; +import { localize } from "../localize"; +import { apiUtil } from "../utils/apiUtil"; + +// tslint:disable-next-line: export-name +export async function createGraphqlApi(context: IActionContext & Partial, node?: ApisTreeItem): Promise { + if (!node) { + const serviceNode = await ext.tree.showTreeItemPicker(ServiceTreeItem.contextValue, context); + node = serviceNode.apisTreeItem; + } + + const link = await askLink(); + const apiName = await apiUtil.askApiName(); + const path = await apiUtil.askPath(); + const body = { + name : apiName, + properties: { + displayName: apiName, + subscriptionRequired: true, + serviceUrl: link, + path: path, + type: "graphql", + protocols: [ + "https" + ] + } + }; + + await window.withProgress( + { + location: ProgressLocation.Notification, + title: localize("", `Creating new Graphql API'...`), + cancellable: false + }, + async () => { + const client: ServiceClient = await createGenericClient(node?.root.credentials); + const response = await client.sendRequest({ + method: "PUT", + body: body, + // tslint:disable-next-line: no-non-null-assertion + url: `https://management.azure.com/subscriptions/${node!.root.subscriptionId}/resourceGroups/${node!.root.resourceGroupName}/providers/Microsoft.ApiManagement/service/${node!.root.serviceName}/apis/${apiName}?api-version=2021-04-01-preview` + }); + + if (response.status !== 200 && response.status !== 201 && response.status !== 202) { + // tslint:disable-next-line: no-non-null-assertion + throw new Error(localize("", response.bodyAsText!)); + } + // tslint:disable-next-line: no-console + console.log(response); + } + ).then(async () => { + window.showInformationMessage(localize("", `New Graphql API has been created!`)); + node?.refresh(context); + }); +} + +async function askLink() : Promise { + const promptStr: string = localize('apiLinkPrompt', 'Specify a Graphql OpenAPI link.'); + return (await ext.ui.showInputBox({ + prompt: promptStr, + placeHolder: 'https://', + validateInput: async (value: string): Promise => { + value = value ? value.trim() : ''; + const regexp = /http(s?):\/\/[\d\w][\d\w]*(\.[\d\w][\d\w-]*)*(:\d+)?(\/[\d\w-\.\?,'/\\\+&=:%\$#_]*)?/; + const isUrlValid = regexp.test(value); + if (!isUrlValid) { + return localize("invalidOpenApiLink", "Provide a valid link. example - https://petstore.swagger.io/v2/swagger.json"); + } else { + return undefined; + } + } + })).trim(); +} diff --git a/src/commands/importGraphqlSchema.ts b/src/commands/importGraphqlSchema.ts new file mode 100644 index 0000000..8f607b3 --- /dev/null +++ b/src/commands/importGraphqlSchema.ts @@ -0,0 +1,73 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ServiceClient } from '@azure/ms-rest-js'; +import * as fse from 'fs-extra'; +import { OpenDialogOptions, ProgressLocation, Uri, window, workspace } from "vscode"; +import { createGenericClient, IActionContext } from "vscode-azureextensionui"; +import { GraphqlApiTreeItem } from "../explorer/GraphqlApiTreeItem"; +import { ServiceTreeItem } from "../explorer/ServiceTreeItem"; +import { ext } from "../extensionVariables"; +import { localize } from '../localize'; + +// tslint:disable-next-line: export-name +export async function importGraphqlSchemaByFile(actionContext: IActionContext, node?: GraphqlApiTreeItem): Promise { + if (!node) { + const serviceNode = await ext.tree.showTreeItemPicker(ServiceTreeItem.contextValue, actionContext); + node = serviceNode; + } + + const uris = await askSchemaDocument(); + const uri = uris[0]; + // tslint:disable-next-line: no-unsafe-any + const fileContent = await fse.readFile(uri.fsPath); + const documentString = fileContent.toString(); + const body = { + properties: { + contentType: "application/vnd.ms-azure-apim.graphql.schema", + document: { + value: documentString + } + } + }; + window.withProgress( + { + location: ProgressLocation.Notification, + title: localize("addSchema", `Add Graphql Schema to Graphql API ${node.root.apiName} ...`), + cancellable: false + }, + // tslint:disable-next-line:no-non-null-assertion + async () => { + const client: ServiceClient = await createGenericClient(node?.root.credentials); + await client.sendRequest({ + method: "PUT", + body: body, + // tslint:disable-next-line: no-non-null-assertion + url: `https://management.azure.com/subscriptions/${node!.root.subscriptionId}/resourceGroups/${node!.root.resourceGroupName}/providers/Microsoft.ApiManagement/service/${node!.root.serviceName}/apis/${node?.root.apiName}/schemas/default?api-version=2021-04-01-preview` + }); + } + ).then(async () => { + // tslint:disable-next-line:no-non-null-assertion + await node!.refresh(actionContext); + window.showInformationMessage(localize("addSchema", `Add Schema to graphql API '${node?.root.apiName}' succesfully.`)); + }); +} + +async function askSchemaDocument(): Promise { + const openDialogOptions: OpenDialogOptions = { + canSelectFiles: true, + canSelectFolders: false, + canSelectMany: false, + openLabel: "Import", + filters: { + JSON: ["js"] + } + }; + const rootPath = workspace.rootPath; + if (rootPath) { + openDialogOptions.defaultUri = Uri.file(rootPath); + } + return await ext.ui.showOpenDialog(openDialogOptions); +} diff --git a/src/commands/showGraphqlAPIQuery.ts b/src/commands/showGraphqlAPIQuery.ts new file mode 100644 index 0000000..dc33a98 --- /dev/null +++ b/src/commands/showGraphqlAPIQuery.ts @@ -0,0 +1,119 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ServiceClient } from '@azure/ms-rest-js'; +import { GraphQLFieldMap, GraphQLInputObjectType, GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLScalarType } from 'graphql'; +import * as vscode from 'vscode'; +import { createGenericClient, IActionContext } from "vscode-azureextensionui"; +import { IApiContract } from '../azure/apim/TempApiContract'; +import { GraphqlObjectTypeTreeItem } from "../explorer/GraphqlObjectTypeTreeItem"; +import { ext } from "../extensionVariables"; +import { createTemporaryFile } from "../utils/fsUtil"; +import { writeToEditor } from '../utils/vscodeUtils'; + +export async function showGraphqlAPIQuery(actionContext: IActionContext, node?: GraphqlObjectTypeTreeItem): Promise { + if (!node) { + node = await ext.tree.showTreeItemPicker(GraphqlObjectTypeTreeItem.contextValue, actionContext); + } + + const query = node.object; + const fileName: string = node.root.apiName.concat("-").concat(query.name).concat(".http"); + const localFilePath: string = await createTemporaryFile(fileName); + let data = ""; + + const client: ServiceClient = await createGenericClient(node?.root.credentials); + const apiContractString = await client.sendRequest({ + method: "GET", + // tslint:disable-next-line: no-non-null-assertion + url: `https://management.azure.com/subscriptions/${node.root.subscriptionId}/resourceGroups/${node.root.resourceGroupName}/providers/Microsoft.ApiManagement/service/${node.root.serviceName}/apis/${node.root.apiName}?api-version=2021-04-01-preview` + }); + // tslint:disable-next-line: no-unsafe-any + const apiTemp : IApiContract = apiContractString.parsedBody; + let queryBuilder = ""; + const serviceUrl = `https://${node.root.serviceName}.azure-api.net/${apiTemp.properties.path}`; + const args = query.args; + let argParams = ""; + let fieldStr = ""; + let queryParams = ""; + let variables = ""; + for (const arg of args) { + if (arg.type instanceof GraphQLScalarType) { + argParams = argParams.concat("$").concat(arg.name).concat(": ").concat(arg.type.name).concat(", "); + queryParams = queryParams.concat(arg.name).concat(": $").concat(`${arg.name}, `); + variables = variables.concat(`"${arg.name}": "", `); + } else if (arg.type instanceof GraphQLNonNull && arg.type.ofType instanceof GraphQLScalarType) { + argParams = argParams.concat("$").concat(arg.name).concat(": ").concat(arg.type.ofType.name).concat("!").concat(", "); + queryParams = queryParams.concat(arg.name).concat(": $").concat(`${arg.name}, `); + variables = variables.concat(`"${arg.name}": "", `); + } else if (arg.type instanceof GraphQLInputObjectType) { + let fieldsStr = ""; + let queryStr = ""; + let varaibleStr = ""; + const argFields = arg.type.getFields(); + for (const argKey of Object.keys(argFields)) { + const argValue = argFields[argKey]; + if (argValue.type instanceof GraphQLScalarType) { + fieldsStr = fieldsStr.concat(`$${argValue.name}: ${argValue.type.name}, `); + queryStr = queryStr.concat(`${argValue.name}: $${argValue.type.name}, `); + varaibleStr = varaibleStr.concat(`"${argValue.name}": "", `); + } + } + fieldsStr = fieldsStr.trimRight(); + fieldsStr = fieldsStr.substring(0, fieldsStr.length - 1); + queryStr = queryStr.trimRight(); + queryStr = queryStr.substring(0, queryStr.length - 1); + varaibleStr = varaibleStr.trimRight(); + varaibleStr = varaibleStr.substring(0, varaibleStr.length - 1); + + argParams = argParams.concat("$").concat(`${arg.name}: { ${fieldsStr} }`).concat(", "); + queryParams = queryParams.concat(`${arg.name}: `).concat(`{ ${queryStr} }`).concat(", "); + variables = variables.concat(`"${arg.name}": `).concat(`{ ${varaibleStr} }`).concat(", "); + } + } + argParams = argParams.trimRight(); + argParams = argParams.substring(0, argParams.length - 1); + queryParams = queryParams.trimRight(); + queryParams = queryParams.substring(0, queryParams.length - 1); + variables = variables.trimRight(); + variables = variables.substring(0, variables.length - 1); + + if (query.type instanceof GraphQLObjectType) { + fieldStr = fieldStr.concat(getFields(query.type.getFields())); + } else if (query.type instanceof GraphQLList && query.type.ofType instanceof GraphQLObjectType) { + fieldStr = fieldStr.concat(getFields(query.type.ofType.getFields())); + } + + queryBuilder = queryBuilder.concat(` query(${argParams}) \n`).concat(`\t{ ${query.name} (${queryParams}) \n\t{ ${fieldStr}}}`); + + const document: vscode.TextDocument = await vscode.workspace.openTextDocument(localFilePath); + //await fse.writeFile(localFilePath, data); + const textEditor: vscode.TextEditor = await vscode.window.showTextDocument(document); + data = data.concat("Post ").concat(serviceUrl).concat("\n\n").concat(`{ "query": "${queryBuilder}", \n\t"variables": {${variables}}}`); + // tslint:disable-next-line: strict-boolean-expressions + if (!!textEditor) { + await writeToEditor(textEditor, data); + await textEditor.document.save(); + } + vscode.commands.executeCommand('setContext', 'isEditorEnabled', true); +} + +// tslint:disable-next-line: no-any +function getFields(fields: GraphQLFieldMap): string { + let resStr = ""; + for (const fieldKey of Object.keys(fields)) { + const fieldValue = fields[fieldKey]; + resStr = resStr.concat(fieldValue.name); + if (fieldValue.type instanceof GraphQLObjectType) { + resStr = resStr.concat(": "); + const childStr = getFields(fieldValue.type.getFields()); + resStr = resStr.concat("{ ").concat(childStr).concat("}, "); + } else { + resStr = resStr.concat(", "); + } + } + resStr = resStr.trimRight(); + resStr = resStr.substring(0, resStr.length - 1); + return resStr; +} diff --git a/src/explorer/ApiTreeItem.ts b/src/explorer/ApiTreeItem.ts index fd371e7..e3f3968 100644 --- a/src/explorer/ApiTreeItem.ts +++ b/src/explorer/ApiTreeItem.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.md in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ApiManagementModels } from "@azure/arm-apimanagement"; import { ApiContract } from "@azure/arm-apimanagement/src/models"; import { ProgressLocation, window } from "vscode"; import { AzureParentTreeItem, AzureTreeItem, DialogResponses, ISubscriptionContext, UserCancelledError } from "vscode-azureextensionui"; @@ -31,7 +30,7 @@ export class ApiTreeItem extends AzureParentTreeItem { constructor( parent: AzureParentTreeItem, - public apiContract: ApiManagementModels.ApiContract, + public apiContract: ApiContract, apiVersion?: string) { super(parent); @@ -42,7 +41,9 @@ export class ApiTreeItem extends AzureParentTreeItem { } this._name = nonNullProp(this.apiContract, 'name'); - this._root = this.createRoot(parent.root, this._name); + this._root = this.apiContract.apiType !== undefined ? + this.createRoot(parent.root, this._name, this.apiContract.apiType!.toString()) + : this.createRoot(parent.root, this._name, undefined); this._operationsTreeItem = new ApiOperationsTreeItem(this); this.policyTreeItem = new ApiPolicyTreeItem(this); } @@ -103,7 +104,7 @@ export class ApiTreeItem extends AzureParentTreeItem { this.apiContract = api; this._name = nonNullProp(api, 'name'); this._label = this.getRevisionDisplayName(api); - this._root = this.createRoot(this.root, this._name); + this._root = this.createRoot(this.root, this._name, this.apiContract.apiType!.toString()); this._operationsTreeItem = new ApiOperationsTreeItem(this); this.policyTreeItem = new ApiPolicyTreeItem(this); } @@ -117,9 +118,10 @@ export class ApiTreeItem extends AzureParentTreeItem { } } - private createRoot(subRoot: ISubscriptionContext, apiName: string): IApiTreeRoot { + private createRoot(subRoot: ISubscriptionContext, apiName: string, apiType: string | undefined): IApiTreeRoot { return Object.assign({}, subRoot, { - apiName: apiName + apiName: apiName, + apiType: apiType }); } } diff --git a/src/explorer/ApisTreeItem.ts b/src/explorer/ApisTreeItem.ts index 7a36794..e254f72 100644 --- a/src/explorer/ApisTreeItem.ts +++ b/src/explorer/ApisTreeItem.ts @@ -5,7 +5,9 @@ import { ApiManagementModels } from "@azure/arm-apimanagement"; import { ApiContract, ApiCreateOrUpdateParameter } from "@azure/arm-apimanagement/src/models"; -import { AzExtTreeItem, AzureParentTreeItem, ICreateChildImplContext } from "vscode-azureextensionui"; +import { ServiceClient } from "@azure/ms-rest-js"; +import { AzExtTreeItem, AzureParentTreeItem, createGenericClient, ICreateChildImplContext } from "vscode-azureextensionui"; +import { IApiContract } from "../azure/apim/TempApiContract"; import { topItemCount } from "../constants"; import { localize } from "../localize"; import { IOpenApiImportObject } from "../openApi/OpenApiImportObject"; @@ -14,6 +16,7 @@ import { processError } from "../utils/errorUtil"; import { treeUtils } from "../utils/treeUtils"; import { ApiTreeItem } from "./ApiTreeItem"; import { ApiVersionSetTreeItem } from "./ApiVersionSetTreeItem"; +import { GraphqlApiTreeItem } from "./GraphqlApiTreeItem"; import { IServiceTreeRoot } from "./IServiceTreeRoot"; export interface IApiTreeItemContext extends ICreateChildImplContext { @@ -45,18 +48,30 @@ export class ApisTreeItem extends AzureParentTreeItem { } let apisToLoad : ApiContract[] = this.selectedApis; + let apisToLoad2 : IApiContract[] = []; if (this.selectedApis.length === 0) { - const apiCollection: ApiManagementModels.ApiCollection = this._nextLink === undefined ? + const apiCollection1: ApiManagementModels.ApiCollection = this._nextLink === undefined ? await this.root.client.api.listByService(this.root.resourceGroupName, this.root.serviceName, { expandApiVersionSet: true, top: topItemCount }) : await this.root.client.api.listByServiceNext(this._nextLink); - this._nextLink = apiCollection.nextLink; + this._nextLink = apiCollection1.nextLink; - apisToLoad = apiCollection.map((s) => s).filter(s => apiUtil.isNotApiRevision(s)); + apisToLoad = apiCollection1.map((s) => s).filter(s => apiUtil.isNotApiRevision(s)); + + const client: ServiceClient = await createGenericClient(this.root.credentials); + const apiCollectionString = await client.sendRequest({ + method: "GET", + // tslint:disable-next-line: no-non-null-assertion + url: `${this.root.environment.resourceManagerEndpointUrl}/subscriptions/${this.root.subscriptionId}/resourceGroups/${this.root.resourceGroupName}/providers/Microsoft.ApiManagement/service/${this.root.serviceName}/apis?api-version=2021-04-01-preview` + }); + // tslint:disable-next-line: no-unsafe-any + const apiCollectionTemp : IApiContract[] = apiCollectionString.parsedBody.value; + apisToLoad2 = this.findGraphqlApis(apiCollectionTemp); } const versionSetMap: Map = new Map(); - return await this.createTreeItemsWithErrorHandling( + // tslint:disable-next-line: no-unnecessary-local-variable + const children1 = await this.createTreeItemsWithErrorHandling( apisToLoad, "invalidApiManagementApi", async (api: ApiManagementModels.ApiContract) => { @@ -81,6 +96,31 @@ export class ApisTreeItem extends AzureParentTreeItem { (api: ApiManagementModels.ApiContract) => { return api.name; }); + + const children2 = await this.createTreeItemsWithErrorHandling( + apisToLoad2, + "invalidApiManagementApi", + async (api: IApiContract) => { + if (api.properties.isCurrent !== undefined && api.properties.isCurrent === true) { + return new GraphqlApiTreeItem(this, api); + } + return undefined; + }, + (api: IApiContract) => { + return api.name; + } + ); + return children1.concat(children2); + } + + public findGraphqlApis(apiCollection : IApiContract[]): IApiContract[] { + const collection : IApiContract[] = []; + for (const api of apiCollection) { + if (api.properties.type === 'graphql') { + collection.push(api); + } + } + return collection; } public async createChildImpl(context: IApiTreeItemContext): Promise { diff --git a/src/explorer/GraphqlApiTreeItem.ts b/src/explorer/GraphqlApiTreeItem.ts new file mode 100644 index 0000000..7a6162f --- /dev/null +++ b/src/explorer/GraphqlApiTreeItem.ts @@ -0,0 +1,139 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ServiceClient } from "@azure/ms-rest-js"; +import { buildSchema, GraphQLField, GraphQLFieldMap, GraphQLNamedType, GraphQLObjectType, GraphQLSchema } from "graphql"; +import { AzureParentTreeItem, AzureTreeItem, createGenericClient, ISubscriptionContext } from "vscode-azureextensionui"; +import { IApiContract } from "../azure/apim/TempApiContract"; +import { TempSchema } from "../azure/apim/TempSchema"; +import { nonNullProp } from "../utils/nonNull"; +import { treeUtils } from "../utils/treeUtils"; +import { ApiPolicyTreeItem } from "./ApiPolicyTreeItem"; +import { GraphqlMutationsTreeItem } from "./GraphqlMutationsTreeItem"; +import { GraphqlQueriesTreeItem } from "./GraphqlQueriesTreeItem"; +//import { GraphqlSubscriptionsTreeItem } from "./GraphqlSubscriptionsTreeItem"; +import { IApiTreeRoot } from "./IApiTreeRoot"; +import { IServiceTreeRoot } from "./IServiceTreeRoot"; + +// tslint:disable: no-any +export class GraphqlApiTreeItem extends AzureParentTreeItem { + public static contextValue: string = 'azureApiManagementGraphql'; + public contextValue: string = GraphqlApiTreeItem.contextValue; + public readonly commandId: string = 'azureApiManagement.showArmApi'; + public policyTreeItem: ApiPolicyTreeItem; + + private _name: string; + private _label: string; + private _root: IApiTreeRoot; + private _queriesTreeItem: GraphqlQueriesTreeItem; + private _mutationsTreeItem: GraphqlMutationsTreeItem; + //private _subscriptionsTreeItem: GraphqlSubscriptionsTreeItem; + + constructor( + parent: AzureParentTreeItem, + public apiContract: IApiContract, + apiVersion?: string) { + super(parent); + + if (!apiVersion) { + const label = nonNullProp(this.apiContract.properties, 'displayName'); + this._label = label.concat(" (GraphqlAPI)"); + } else { + this._label = apiVersion.concat(" (GraphqlAPI)"); + } + + this._name = nonNullProp(this.apiContract, 'name'); + this._root = this.createRoot(parent.root, this._name, this.apiContract.properties.type); + this.policyTreeItem = new ApiPolicyTreeItem(this); + } + + public get id(): string { + return this._label; + } + + public get label(): string { + return this._label; + } + + public get root(): IApiTreeRoot { + return this._root; + } + + public get iconPath(): { light: string, dark: string } { + return treeUtils.getThemedIconPath('api'); + } + + public async loadMoreChildrenImpl(): Promise[]> { + const client: ServiceClient = await createGenericClient(this.root.credentials); + const schemasString = await client.sendRequest({ + method: "GET", + // tslint:disable-next-line: no-non-null-assertion + url: `https://management.azure.com/subscriptions/${this.root.subscriptionId}/resourceGroups/${this.root.resourceGroupName}/providers/Microsoft.ApiManagement/service/${this.root.serviceName}/apis/${this.root.apiName}/schemas?api-version=2021-04-01-preview` + }); + // tslint:disable: no-unsafe-any + // tslint:disable-next-line: no-unnecessary-local-variable + const schemas : TempSchema[] = schemasString.parsedBody.value; + + const valueList: GraphQLSchema[] = []; + // tslint:disable-next-line: no-any + const queryTypes: GraphQLField[] = []; + const mutationTypes: GraphQLField[] = []; + const subscriptionTypes: GraphQLField[] = []; + for (const schema of schemas) { + const builtSchema : GraphQLSchema = buildSchema(schema.properties.document.value); + valueList.push(builtSchema); + const typeMap = builtSchema.getTypeMap(); + for (const key of Object.keys(typeMap)) { + const value: GraphQLNamedType = typeMap[key]; + if (key === "Query" && value instanceof GraphQLObjectType) { + const fields: GraphQLFieldMap = value.getFields(); + for (const fieldKey of Object.keys(fields)) { + const fieldValue = fields[fieldKey]; + queryTypes.push(fieldValue); + } + } else if (key === "Mutation" && value instanceof GraphQLObjectType) { + const fields: GraphQLFieldMap = value.getFields(); + for (const fieldKey of Object.keys(fields)) { + const fieldValue = fields[fieldKey]; + mutationTypes.push(fieldValue); + } + } else if (key === "Subscription" && value instanceof GraphQLObjectType) { + const fields: GraphQLFieldMap = value.getFields(); + for (const fieldKey of Object.keys(fields)) { + const fieldValue = fields[fieldKey]; + subscriptionTypes.push(fieldValue); + } + } + } + } + this._queriesTreeItem = new GraphqlQueriesTreeItem(this, queryTypes); + this._mutationsTreeItem = new GraphqlMutationsTreeItem(this, mutationTypes); + /*this._subscriptionsTreeItem = new GraphqlSubscriptionsTreeItem(this, subscriptionTypes); + + return [this._queriesTreeItem, this._mutationsTreeItem, this._subscriptionsTreeItem, this.policyTreeItem]; + */ + return [this._queriesTreeItem, this._mutationsTreeItem, this.policyTreeItem]; + } + + public hasMoreChildrenImpl(): boolean { + return false; + } + + private createRoot(subRoot: ISubscriptionContext, apiName: string, apiType: string): IApiTreeRoot { + return Object.assign({}, subRoot, { + apiName: apiName, + apiType: apiType + }); + } +} diff --git a/src/explorer/GraphqlArgFieldTreeItem.ts b/src/explorer/GraphqlArgFieldTreeItem.ts new file mode 100644 index 0000000..d146257 --- /dev/null +++ b/src/explorer/GraphqlArgFieldTreeItem.ts @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { GraphQLInputField } from "graphql"; +import { AzureParentTreeItem, AzureTreeItem } from "vscode-azureextensionui"; +import { localize } from "../localize"; +import { treeUtils } from "../utils/treeUtils"; +import { IApiTreeRoot } from "./IApiTreeRoot"; + +// tslint:disable: no-any +export class GraphqlArgFieldTreeItem extends AzureTreeItem { + + public get iconPath(): { light: string, dark: string } { + return treeUtils.getThemedIconPath('op'); + } + public static contextValue: string = 'azureApiManagementGraphqlArgFieldList'; + public _label: string; + public contextValue: string = GraphqlArgFieldTreeItem.contextValue; + public readonly childTypeLabel: string = localize('azureApiManagement.graphqlArgFieldList', 'graphqlArgFieldList'); + public argPath: string[]; + + private argField: GraphQLInputField; + + public get label() : string { + return this._label; + } + + constructor( + parent: AzureParentTreeItem, + // tslint:disable-next-line: no-any + argField: GraphQLInputField, + argPath: string[]) { + super(parent); + this.argField = argField; + this._label = this.argField.name; + this.argPath = argPath; + this.argPath.push(this.argField.name); + } +} diff --git a/src/explorer/GraphqlArgsLeafTreeItem.ts b/src/explorer/GraphqlArgsLeafTreeItem.ts new file mode 100644 index 0000000..d32dee4 --- /dev/null +++ b/src/explorer/GraphqlArgsLeafTreeItem.ts @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { GraphQLArgument } from "graphql"; +import { AzureParentTreeItem, AzureTreeItem } from "vscode-azureextensionui"; +import { localize } from "../localize"; +import { treeUtils } from "../utils/treeUtils"; +import { IApiTreeRoot } from "./IApiTreeRoot"; + +// tslint:disable: no-any +export class GraphqlArgsLeafTreeItem extends AzureTreeItem { + + public get iconPath(): { light: string, dark: string } { + return treeUtils.getThemedIconPath('op'); + } + public static contextValue: string = 'azureApiManagementGraphqlArgsLeaf'; + public _label: string; + public contextValue: string = GraphqlArgsLeafTreeItem.contextValue; + public readonly childTypeLabel: string = localize('azureApiManagement.graphqlArgsLeaf', 'graphqlArgsLeaf'); + public argPath: string[]; + + private arg: GraphQLArgument; + + public get label() : string { + return this._label; + } + + constructor( + parent: AzureParentTreeItem, + // tslint:disable-next-line: no-any + arg: GraphQLArgument, + argPath: string[]) { + super(parent); + this.arg = arg; + this._label = this.arg.name; + this.argPath = argPath; + this.argPath.push(arg.name); + } +} diff --git a/src/explorer/GraphqlArgsTreeItem.ts b/src/explorer/GraphqlArgsTreeItem.ts new file mode 100644 index 0000000..2c91667 --- /dev/null +++ b/src/explorer/GraphqlArgsTreeItem.ts @@ -0,0 +1,68 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { GraphQLArgument, GraphQLInputField, GraphQLInputFieldMap, GraphQLInputObjectType, GraphQLInputType } from "graphql"; +import { AzExtTreeItem, AzureParentTreeItem } from "vscode-azureextensionui"; +import { localize } from "../localize"; +import { treeUtils } from "../utils/treeUtils"; +import { GraphqlArgFieldTreeItem } from "./GraphqlArgFieldTreeItem"; +import { IApiTreeRoot } from "./IApiTreeRoot"; + +// tslint:disable: no-any +export class GraphqlArgsTreeItem extends AzureParentTreeItem { + + public get iconPath(): { light: string, dark: string } { + return treeUtils.getThemedIconPath('op'); + } + public static contextValue: string = 'azureApiManagementGraphqlArgsList'; + public _label: string; + public contextValue: string = GraphqlArgsTreeItem.contextValue; + public readonly childTypeLabel: string = localize('azureApiManagement.graphqlArgsList', 'graphqlArgsList'); + public argPath: string[]; + private _nextLink: string | undefined; + + private arg: GraphQLArgument; + + public get label() : string { + return this._label; + } + + constructor( + parent: AzureParentTreeItem, + // tslint:disable-next-line: no-any + arg: GraphQLArgument, + argPath: string[]) { + super(parent); + this.arg = arg; + this._label = this.arg.name; + this.argPath = argPath; + this.argPath.push(arg.name); + } + + public hasMoreChildrenImpl(): boolean { + return this._nextLink !== undefined; + } + + public async loadMoreChildrenImpl(clearCache: boolean): Promise { + if (clearCache) { + this._nextLink = undefined; + } + const args : GraphQLInputType = this.arg.type; + if (args instanceof GraphQLInputObjectType) { + const fields : GraphQLInputFieldMap = args.getFields(); + const fieldValues : GraphQLInputField[] = Object.values(fields); + return await this.createTreeItemsWithErrorHandling( + fieldValues, + "invalidApiManagementGraphqlObjectTypes", + async (objectType: GraphQLInputField) => { + return new GraphqlArgFieldTreeItem(this, objectType, this.argPath); + } , + (objectType: GraphQLInputField) => { + return objectType.name; + }); + } + return []; + } +} diff --git a/src/explorer/GraphqlFieldsLeafTreeItem.ts b/src/explorer/GraphqlFieldsLeafTreeItem.ts new file mode 100644 index 0000000..b8414eb --- /dev/null +++ b/src/explorer/GraphqlFieldsLeafTreeItem.ts @@ -0,0 +1,47 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { GraphQLField } from "graphql"; +import { AzureParentTreeItem, AzureTreeItem } from "vscode-azureextensionui"; +import { localize } from "../localize"; +import { treeUtils } from "../utils/treeUtils"; +import { IApiTreeRoot } from "./IApiTreeRoot"; + +// tslint:disable: no-any +export class GraphqlFieldsLeafTreeItem extends AzureTreeItem { + + public get iconPath(): { light: string, dark: string } { + return treeUtils.getThemedIconPath('leafnode'); + } + + public get label() : string { + return this._label; + } + public static contextValue: string = 'azureApiManagementGraphqlFieldsLeafNode'; + public _label: string; + public contextValue: string = GraphqlFieldsLeafTreeItem.contextValue; + public readonly childTypeLabel: string = localize('azureApiManagement.graphqlFieldsLeafNode', 'graphqlFieldsLeafNode'); + public fieldPath: string[]; + + private field: GraphQLField; + + constructor( + parent: AzureParentTreeItem, + // tslint:disable-next-line: no-any + field: GraphQLField, + fieldPath: string[]) { + super(parent); + this.field = field; + this._label = this.field.name; + this.fieldPath = fieldPath; + this.fieldPath.push(this.field.name); + } +} diff --git a/src/explorer/GraphqlFieldsTreeItem.ts b/src/explorer/GraphqlFieldsTreeItem.ts new file mode 100644 index 0000000..7959ebb --- /dev/null +++ b/src/explorer/GraphqlFieldsTreeItem.ts @@ -0,0 +1,81 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { GraphQLField, GraphQLObjectType } from "graphql"; +import { AzExtTreeItem, AzureParentTreeItem } from "vscode-azureextensionui"; +import { localize } from "../localize"; +import { treeUtils } from "../utils/treeUtils"; +import { GraphqlFieldsLeafTreeItem } from "./GraphqlFieldsLeafTreeItem"; +import { IApiTreeRoot } from "./IApiTreeRoot"; + +// tslint:disable: no-any +export class GraphqlFieldsTreeItem extends AzureParentTreeItem { + + public get iconPath(): { light: string, dark: string } { + return treeUtils.getThemedIconPath('treelist'); + } + + public get label() : string { + return this._label; + } + public static contextValue: string = 'azureApiManagementGraphqlFieldsList'; + public _label: string; + public contextValue: string = GraphqlFieldsTreeItem.contextValue; + public readonly childTypeLabel: string = localize('azureApiManagement.graphqlFieldsList', 'graphqlFieldsList'); + public fieldPath: string[]; + private _nextLink: string | undefined; + + private field: GraphQLField; + + constructor( + parent: AzureParentTreeItem, + // tslint:disable-next-line: no-any + field: GraphQLField, + fieldPath: string[]) { + super(parent); + this.field = field; + this._label = this.field.name; + this.fieldPath = fieldPath; + this.fieldPath.push(this.field.name); + } + + public hasMoreChildrenImpl(): boolean { + return this._nextLink !== undefined; + } + + public async loadMoreChildrenImpl(clearCache: boolean): Promise { + if (clearCache) { + this._nextLink = undefined; + } + const fieldChildren = this.field.type; + if (fieldChildren instanceof GraphQLObjectType) { + const fields = Object.values(fieldChildren.getFields()); + return await this.createTreeItemsWithErrorHandling( + fields, + "invalidApiManagementGraphqlObjectTypes", + async (objectType: GraphQLField) => { + if (objectType.type instanceof GraphQLObjectType) { + return new GraphqlFieldsTreeItem(this, objectType, this.fieldPath); + } else { + return new GraphqlFieldsLeafTreeItem(this, objectType, this.fieldPath); + } + }, + (objectType: GraphQLField) => { + return objectType.name; + }); + } + return []; + } +} diff --git a/src/explorer/GraphqlMutationsTreeItem.ts b/src/explorer/GraphqlMutationsTreeItem.ts new file mode 100644 index 0000000..9ce8305 --- /dev/null +++ b/src/explorer/GraphqlMutationsTreeItem.ts @@ -0,0 +1,65 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { GraphQLField } from "graphql"; +import { AzExtTreeItem, AzureParentTreeItem } from "vscode-azureextensionui"; +import { localize } from "../localize"; +import { treeUtils } from "../utils/treeUtils"; +import { GraphqlObjectTypeTreeItem } from "./GraphqlObjectTypeTreeItem"; +import { IApiTreeRoot } from "./IApiTreeRoot"; + +// tslint:disable: no-any +export class GraphqlMutationsTreeItem extends AzureParentTreeItem { + + public get iconPath(): { light: string, dark: string } { + return treeUtils.getThemedIconPath('mutation'); + } + public static contextValue: string = 'azureApiManagementGraphqlMutationsList'; + public label: string = "Mutation"; + public contextValue: string = GraphqlMutationsTreeItem.contextValue; + public readonly childTypeLabel: string = localize('azureApiManagement.graphqlMutationList', 'graphqlMutationList'); + private _nextLink: string | undefined; + + private mutationTypes: GraphQLField[]; + + constructor( + parent: AzureParentTreeItem, + // tslint:disable-next-line: no-any + mutationTypes: GraphQLField[]) { + super(parent); + this.mutationTypes = mutationTypes; + } + + public hasMoreChildrenImpl(): boolean { + return this._nextLink !== undefined; + } + + public async loadMoreChildrenImpl(clearCache: boolean): Promise { + if (clearCache) { + this._nextLink = undefined; + } + + return this.createTreeItemsWithErrorHandling( + this.mutationTypes, + "invalidApiManagementGraphqlObjectTypes", + async (objectType: GraphQLField) => new GraphqlObjectTypeTreeItem(this, objectType), + (objectType: GraphQLField) => { + return objectType.name; + }); + + } +} diff --git a/src/explorer/GraphqlObjectTypeTreeItem.ts b/src/explorer/GraphqlObjectTypeTreeItem.ts new file mode 100644 index 0000000..beb5617 --- /dev/null +++ b/src/explorer/GraphqlObjectTypeTreeItem.ts @@ -0,0 +1,117 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { GraphQLArgument, GraphQLField, GraphQLFieldMap, GraphQLInputObjectType, GraphQLList, GraphQLObjectType, GraphQLOutputType } from "graphql"; +import { AzExtTreeItem, AzureParentTreeItem } from "vscode-azureextensionui"; +import { treeUtils } from "../utils/treeUtils"; +import { GraphqlArgsLeafTreeItem } from "./GraphqlArgsLeafTreeItem"; +import { GraphqlArgsTreeItem } from "./GraphqlArgsTreeItem"; +import { GraphqlFieldsLeafTreeItem } from "./GraphqlFieldsLeafTreeItem"; +import { GraphqlFieldsTreeItem } from "./GraphqlFieldsTreeItem"; +import { IApiTreeRoot } from "./IApiTreeRoot"; + +// tslint:disable: no-any +export class GraphqlObjectTypeTreeItem extends AzureParentTreeItem { + public static contextValue: string = 'azureApiManagementGraphqlObjectType'; + public contextValue: string = GraphqlObjectTypeTreeItem.contextValue; + public readonly commandId: string = 'azureApiManagement.showGraphqlAPIQuery'; + + private _name: string; + private _label: string; + + public get iconPath(): { light: string, dark: string } { + return treeUtils.getThemedIconPath('treelist'); + } + + public get label() : string { + return this._label; + } + + public get id(): string { + return this._name; + } + + constructor( + parent: AzureParentTreeItem, + // tslint:disable-next-line: no-any + public readonly object: GraphQLField) { + super(parent); + this._label = object.name; + this._name = object.name; + } + + public async loadMoreChildrenImpl(): Promise { + const args: GraphQLArgument[] = this.object.args; + let allNodes: AzExtTreeItem[] = []; + const argsNodes = await this.createTreeItemsWithErrorHandling( + args, + "invalidApiManagementGraphqlObjectTypes", + async (objectType: GraphQLArgument) => { + if (objectType.type instanceof GraphQLInputObjectType) { + return new GraphqlArgsTreeItem(this, objectType, [this.object.name]); + } else { + return new GraphqlArgsLeafTreeItem(this, objectType, [this.object.name]); + } + }, + (objectType: GraphQLArgument) => { + return objectType.name; + }); + allNodes = allNodes.concat(argsNodes); + + const types: GraphQLOutputType = this.object.type; + if (types instanceof GraphQLList && types.ofType instanceof GraphQLObjectType) { + const objectTypes: GraphQLObjectType = types.ofType; + const fields: GraphQLFieldMap = objectTypes.getFields(); + const fieldValues = Object.values(fields); + const fieldNodes = await this.createTreeItemsWithErrorHandling( + fieldValues, + "invalidApiManagementGraphqlObjectTypes", + async (objectType: GraphQLField) => { + if (objectType.type instanceof GraphQLObjectType) { + return new GraphqlFieldsTreeItem(this, objectType, [this.object.name]); + } else { + return new GraphqlFieldsLeafTreeItem(this, objectType, [this.object.name]); + } + }, + (objectType: GraphQLField) => { + return objectType.name; + }); + allNodes = allNodes.concat(fieldNodes); + } else if (types instanceof GraphQLObjectType) { + const fields: GraphQLFieldMap = types.getFields(); + const fieldValues = Object.values(fields); + const fieldNodes = await this.createTreeItemsWithErrorHandling( + fieldValues, + "invalidApiManagementGraphqlObjectTypes", + async (objectType: GraphQLField) => { + if (objectType.type instanceof GraphQLObjectType) { + return new GraphqlFieldsTreeItem(this, objectType, [this.object.name]); + } else { + return new GraphqlFieldsLeafTreeItem(this, objectType, [this.object.name]); + } + }, + (objectType: GraphQLField) => { + return objectType.name; + }); + allNodes = allNodes.concat(fieldNodes); + } + return allNodes; + } + + public hasMoreChildrenImpl(): boolean { + return false; + } +} diff --git a/src/explorer/GraphqlQueriesTreeItem.ts b/src/explorer/GraphqlQueriesTreeItem.ts new file mode 100644 index 0000000..0018bec --- /dev/null +++ b/src/explorer/GraphqlQueriesTreeItem.ts @@ -0,0 +1,59 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { GraphQLField } from "graphql"; +import { AzExtTreeItem, AzureParentTreeItem } from "vscode-azureextensionui"; +import { localize } from "../localize"; +import { treeUtils } from "../utils/treeUtils"; +import { GraphqlObjectTypeTreeItem } from "./GraphqlObjectTypeTreeItem"; +import { IApiTreeRoot } from "./IApiTreeRoot"; + +// tslint:disable: no-any +export class GraphqlQueriesTreeItem extends AzureParentTreeItem { + + public get iconPath(): { light: string, dark: string } { + return treeUtils.getThemedIconPath('querylist'); + } + public static contextValue: string = 'azureApiManagementGraphqlList'; + public label: string = "Query"; + public contextValue: string = GraphqlQueriesTreeItem.contextValue; + public readonly childTypeLabel: string = localize('azureApiManagement.graphqlList', 'graphqlList'); + private _nextLink: string | undefined; + + private queryTypes: GraphQLField[]; + + constructor( + parent: AzureParentTreeItem, + queryTypes: GraphQLField[]) { + super(parent); + this.queryTypes = queryTypes; + } + + public hasMoreChildrenImpl(): boolean { + return this._nextLink !== undefined; + } + + public async loadMoreChildrenImpl(clearCache: boolean): Promise { + if (clearCache) { + this._nextLink = undefined; + } + + return this.createTreeItemsWithErrorHandling( + this.queryTypes, + "invalidApiManagementGraphqlObjectTypes", + async (objectType: GraphQLField) => new GraphqlObjectTypeTreeItem(this, objectType), + (objectType: GraphQLField) => { + return objectType.name; + }); + } +} diff --git a/src/explorer/GraphqlSchemaTreeItem.ts b/src/explorer/GraphqlSchemaTreeItem.ts new file mode 100644 index 0000000..d772020 --- /dev/null +++ b/src/explorer/GraphqlSchemaTreeItem.ts @@ -0,0 +1,126 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ApiContract } from "@azure/arm-apimanagement/src/models"; +import { ProgressLocation, window } from "vscode"; +import { AzureParentTreeItem, AzureTreeItem, DialogResponses, ISubscriptionContext, UserCancelledError } from "vscode-azureextensionui"; +import { localize } from "../localize"; +import { nonNullProp } from "../utils/nonNull"; +import { treeUtils } from "../utils/treeUtils"; +import { ApiOperationsTreeItem } from "./ApiOperationsTreeItem"; +import { ApiOperationTreeItem } from "./ApiOperationTreeItem"; +import { ApiPolicyTreeItem } from "./ApiPolicyTreeItem"; +import { IApiTreeRoot } from "./IApiTreeRoot"; +import { IServiceTreeRoot } from "./IServiceTreeRoot"; +import { OperationPolicyTreeItem } from "./OperationPolicyTreeItem"; + +// tslint:disable: no-non-null-assertion +export class GraphqlSchemaTreeItem extends AzureParentTreeItem { + public static contextValue: string = 'azureApiManagementGraphqlSchema'; + public contextValue: string = GraphqlSchemaTreeItem.contextValue; + public policyTreeItem: ApiPolicyTreeItem; + + private _name: string; + private _label: string; + private _root: IApiTreeRoot; + private _operationsTreeItem: ApiOperationsTreeItem; + + constructor( + parent: AzureParentTreeItem, + public apiContract: ApiContract, + apiVersion?: string) { + super(parent); + + if (!apiVersion) { + this._label = nonNullProp(this.apiContract, 'displayName'); + } else { + this._label = apiVersion; + } + + this._name = nonNullProp(this.apiContract, 'name'); + this._root = this.apiContract.apiType !== undefined ? + this.createRoot(parent.root, this._name, this.apiContract.apiType!.toString()) + : this.createRoot(parent.root, this._name, undefined); + this._operationsTreeItem = new ApiOperationsTreeItem(this); + this.policyTreeItem = new ApiPolicyTreeItem(this); + } + + public get id(): string { + return this._name; + } + + public get label() : string { + return this._label; + } + + public get root(): IApiTreeRoot { + return this._root; + } + + public get iconPath(): { light: string, dark: string } { + return treeUtils.getThemedIconPath('api'); + } + + public async loadMoreChildrenImpl(): Promise[]> { + return [this._operationsTreeItem, this.policyTreeItem]; + } + + public hasMoreChildrenImpl(): boolean { + return false; + } + + public async deleteTreeItemImpl(): Promise { + const message: string = localize("confirmDeleteApi", `Are you sure you want to delete API '${this.root.apiName}' and its contents?`); + const result = await window.showWarningMessage(message, { modal: true }, DialogResponses.deleteResponse, DialogResponses.cancel); + if (result === DialogResponses.deleteResponse) { + const deletingMessage: string = localize("deletingApi", `Deleting API "${this.root.apiName}"...`); + await window.withProgress({ location: ProgressLocation.Notification, title: deletingMessage }, async () => { + await this.root.client.api.deleteMethod(this.root.resourceGroupName, this.root.serviceName, this.root.apiName, '*'); + }); + // don't wait + window.showInformationMessage(localize("deletedApi", `Successfully deleted API "${this.root.apiName}".`)); + + } else { + throw new UserCancelledError(); + } + } + + public pickTreeItemImpl(expectedContextValues: (string | RegExp)[]): AzureTreeItem | undefined { + for (const expectedContextValue of expectedContextValues) { + switch (expectedContextValue) { + case OperationPolicyTreeItem.contextValue: + case ApiOperationTreeItem.contextValue: + return this._operationsTreeItem; + default: + } + } + return undefined; + } + + public async reloadApi(api: ApiContract): Promise { + this.apiContract = api; + this._name = nonNullProp(api, 'name'); + this._label = this.getRevisionDisplayName(api); + this._root = this.createRoot(this.root, this._name, this.apiContract.apiType!.toString()); + this._operationsTreeItem = new ApiOperationsTreeItem(this); + this.policyTreeItem = new ApiPolicyTreeItem(this); + } + + private getRevisionDisplayName(api: ApiContract): string { + if (api.isCurrent !== undefined && api.isCurrent === true) { + return api.displayName!; + } else { + const revNumber = api.name!.split(';rev=')[1]; + return api.displayName!.concat(';rev=', revNumber); + } + } + + private createRoot(subRoot: ISubscriptionContext, apiName: string, apiType: string | undefined): IApiTreeRoot { + return Object.assign({}, subRoot, { + apiName: apiName, + apiType: apiType + }); + } +} diff --git a/src/explorer/GraphqlSubscriptionsTreeItem.ts b/src/explorer/GraphqlSubscriptionsTreeItem.ts new file mode 100644 index 0000000..e29a4f3 --- /dev/null +++ b/src/explorer/GraphqlSubscriptionsTreeItem.ts @@ -0,0 +1,65 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { GraphQLField } from "graphql"; +import { AzExtTreeItem, AzureParentTreeItem } from "vscode-azureextensionui"; +import { localize } from "../localize"; +import { treeUtils } from "../utils/treeUtils"; +import { GraphqlObjectTypeTreeItem } from "./GraphqlObjectTypeTreeItem"; +import { IApiTreeRoot } from "./IApiTreeRoot"; + +// tslint:disable: no-any +export class GraphqlSubscriptionsTreeItem extends AzureParentTreeItem { + + public get iconPath(): { light: string, dark: string } { + return treeUtils.getThemedIconPath('list'); + } + public static contextValue: string = 'azureApiManagementGraphqlSubscriptionsList'; + public label: string = "Subscription"; + public contextValue: string = GraphqlSubscriptionsTreeItem.contextValue; + public readonly childTypeLabel: string = localize('azureApiManagement.graphqlSubscriptionList', 'graphqlSubscriptionList'); + private _nextLink: string | undefined; + + private mutationTypes: GraphQLField[]; + + constructor( + parent: AzureParentTreeItem, + // tslint:disable-next-line: no-any + mutationTypes: GraphQLField[]) { + super(parent); + this.mutationTypes = mutationTypes; + } + + public hasMoreChildrenImpl(): boolean { + return this._nextLink !== undefined; + } + + public async loadMoreChildrenImpl(clearCache: boolean): Promise { + if (clearCache) { + this._nextLink = undefined; + } + + return this.createTreeItemsWithErrorHandling( + this.mutationTypes, + "invalidApiManagementGraphqlObjectTypes", + async (objectType: GraphQLField) => new GraphqlObjectTypeTreeItem(this, objectType), + (objectType: GraphQLField) => { + return objectType.name; + }); + + } +} diff --git a/src/explorer/IApiTreeRoot.ts b/src/explorer/IApiTreeRoot.ts index 05414bb..7f4c6ac 100644 --- a/src/explorer/IApiTreeRoot.ts +++ b/src/explorer/IApiTreeRoot.ts @@ -7,4 +7,5 @@ import { IServiceTreeRoot } from "./IServiceTreeRoot"; export interface IApiTreeRoot extends IServiceTreeRoot { apiName: string; + apiType: string | undefined; } diff --git a/src/explorer/editors/arm/ApiResourceEditor.ts b/src/explorer/editors/arm/ApiResourceEditor.ts index 7a7487a..9d22cf5 100644 --- a/src/explorer/editors/arm/ApiResourceEditor.ts +++ b/src/explorer/editors/arm/ApiResourceEditor.ts @@ -4,7 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { ApiManagementModels } from "@azure/arm-apimanagement"; -import { AzureTreeItem } from "vscode-azureextensionui"; +import { ServiceClient } from "@azure/ms-rest-js"; +import { AzureTreeItem, createGenericClient } from "vscode-azureextensionui"; +import { IApiContract } from "../../../azure/apim/TempApiContract"; import { IApiTreeRoot } from "../../IApiTreeRoot"; import { BaseArmResourceEditor } from "./BaseArmResourceEditor"; @@ -15,11 +17,36 @@ export class ApiResourceEditor extends BaseArmResourceEditor { super(); } - public async getDataInternal(context: AzureTreeItem): Promise { + public async getDataInternal(context: AzureTreeItem): Promise { + if (context.root.apiType !== undefined && context.root.apiType === 'graphql') { + const client: ServiceClient = await createGenericClient(context.root.credentials); + const apiString = await client.sendRequest({ + method: "GET", + // tslint:disable-next-line: no-non-null-assertion + url: `https://management.azure.com/subscriptions/${context.root.subscriptionId}/resourceGroups/${context.root.resourceGroupName}/providers/Microsoft.ApiManagement/service/${context.root.serviceName}/apis/${context.root.apiName}?api-version=2021-04-01-preview` + }); + // tslint:disable: no-unsafe-any + // tslint:disable-next-line: no-unnecessary-local-variable + const apiTemp : IApiContract = apiString.parsedBody; + return apiTemp; + } return await context.root.client.api.get(context.root.resourceGroupName, context.root.serviceName, context.root.apiName); } - public async updateDataInternal(context: AzureTreeItem, payload: ApiManagementModels.ApiCreateOrUpdateParameter): Promise { + public async updateDataInternal(context: AzureTreeItem, payload: ApiManagementModels.ApiCreateOrUpdateParameter): Promise { + if (context.root.apiType !== undefined && context.root.apiType === 'graphql') { + const client: ServiceClient = await createGenericClient(context.root.credentials); + const apiString = await client.sendRequest({ + method: "PUT", + body: payload, + // tslint:disable-next-line: no-non-null-assertion + url: `https://management.azure.com/subscriptions/${context.root.subscriptionId}/resourceGroups/${context.root.resourceGroupName}/providers/Microsoft.ApiManagement/service/${context.root.serviceName}/apis/${context.root.apiName}?api-version=2021-04-01-preview` + }); + // tslint:disable: no-unsafe-any + // tslint:disable-next-line: no-unnecessary-local-variable + const apiTemp : IApiContract = apiString.parsedBody; + return apiTemp; + } return await context.root.client.api.createOrUpdate(context.root.resourceGroupName, context.root.serviceName, context.root.apiName, payload); } } diff --git a/src/explorer/editors/graphqlApiSchema/GraphqlApiSchemaEditor.ts b/src/explorer/editors/graphqlApiSchema/GraphqlApiSchemaEditor.ts new file mode 100644 index 0000000..9562982 --- /dev/null +++ b/src/explorer/editors/graphqlApiSchema/GraphqlApiSchemaEditor.ts @@ -0,0 +1,74 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ServiceClient } from "@azure/ms-rest-js"; +import { ProgressLocation, window } from "vscode"; +import { createGenericClient } from "vscode-azureextensionui"; +import { showSavePromptConfigKey } from "../../../constants"; +import { localize } from "../../../localize"; +import { GraphqlApiTreeItem } from "../../GraphqlApiTreeItem"; +import { Editor } from "../Editor"; + +// tslint:disable: no-unsafe-any +export class GraphqlApiSchemaEditor extends Editor { + constructor() { + super(showSavePromptConfigKey); + } + + public async getData(context: GraphqlApiTreeItem): Promise { + const client: ServiceClient = await createGenericClient(context.root.credentials); + const schemasString = await client.sendRequest({ + method: "GET", + // tslint:disable-next-line: no-non-null-assertion + url: `https://management.azure.com/subscriptions/${context.root.subscriptionId}/resourceGroups/${context.root.resourceGroupName}/providers/Microsoft.ApiManagement/service/${context.root.serviceName}/apis/${context.root.apiName}/schemas/default?api-version=2021-04-01-preview` + }); + return schemasString.parsedBody.properties.document.value; + } + + public async updateData(context: GraphqlApiTreeItem, data: string): Promise { + return window.withProgress( + { + location: ProgressLocation.Notification, + title: localize("updateAPISchema", `Applying changes to Graphql API schema '${context.root.apiName}' in API Management instance ${context.root.serviceName}...`), + cancellable: false + }, + async () => { + const body = { + properties: { + contentType: "application/vnd.ms-azure-apim.graphql.schema", + document: { + value: data + } + } + }; + + const client: ServiceClient = await createGenericClient(context.root.credentials); + await client.sendRequest({ + method: "PUT", + body: body, + // tslint:disable-next-line: no-non-null-assertion + url: `https://management.azure.com/subscriptions/${context.root.subscriptionId}/resourceGroups/${context.root.resourceGroupName}/providers/Microsoft.ApiManagement/service/${context.root.serviceName}/apis/${context.root.apiName}/schemas/default?api-version=2021-04-01-preview` + }); + } + ).then(async () => { + window.showInformationMessage(localize("updateAPISchema", `Changes to API '${context.apiContract.name}' were succefully uploaded to cloud.`)); + //await context.refresh(); + return this.getData(context); + }); + } + + public async getFilename(context: GraphqlApiTreeItem): Promise { + return `${context.root.serviceName}-${context.root.apiName}-schema.js`; + } + public async getDiffFilename(context: GraphqlApiTreeItem): Promise { + return `${context.root.serviceName}-${context.root.apiName}-schema-temp.js`; + } + public async getSaveConfirmationText(context: GraphqlApiTreeItem): Promise { + return localize("", `Saving will update the Grapql API Schema '${context.apiContract.name}'.`); + } + public async getSize(_context: GraphqlApiTreeItem): Promise { + throw new Error("Method not implemented."); + } +} diff --git a/src/explorer/editors/policy/ApiPolicyEditor.ts b/src/explorer/editors/policy/ApiPolicyEditor.ts index 2a86e19..dc63da2 100644 --- a/src/explorer/editors/policy/ApiPolicyEditor.ts +++ b/src/explorer/editors/policy/ApiPolicyEditor.ts @@ -4,20 +4,53 @@ *--------------------------------------------------------------------------------------------*/ import { ApiManagementModels } from "@azure/arm-apimanagement"; -import { AzureTreeItem } from "vscode-azureextensionui"; +import { ServiceClient } from "@azure/ms-rest-js"; +import { AzureTreeItem, createGenericClient } from "vscode-azureextensionui"; import { emptyPolicyXml, policyFormat } from "../../../constants"; import { IApiTreeRoot } from "../../IApiTreeRoot"; import { BasePolicyEditor } from "./BasePolicyEditor"; export class ApiPolicyEditor extends BasePolicyEditor { public async getPolicy(context: AzureTreeItem): Promise { + if (context.root.apiType !== undefined && context.root.apiType === 'graphql') { + + const client: ServiceClient = await createGenericClient(context.root.credentials); + const policyString = await client.sendRequest({ + method: "GET", + // tslint:disable-next-line: no-non-null-assertion + url: `https://management.azure.com/subscriptions/${context.root.subscriptionId}/resourceGroups/${context.root.resourceGroupName}/providers/Microsoft.ApiManagement/service/${context.root.serviceName}/apis/${context.root.apiName}/policies/policy?api-version=2021-04-01-preview` + }); + // tslint:disable: no-unsafe-any + // tslint:disable-next-line: no-unnecessary-local-variable + const policyTemp = policyString.parsedBody; + return policyTemp.properties.value; + } const policy = await context.root.client.apiPolicy.get(context.root.resourceGroupName, context.root.serviceName, context.root.apiName, { format: policyFormat }); return policy._response.bodyAsText; } public async updatePolicy(context: AzureTreeItem, policy: ApiManagementModels.PolicyContract): Promise { - const policyResult = await context.root.client.apiPolicy.createOrUpdate(context.root.resourceGroupName, context.root.serviceName, context.root.apiName, policy); - return policyResult._response.bodyAsText; + if (context.root.apiType !== undefined && context.root.apiType === 'graphql') { + const body = { + properties: { + value: policy.value, + format: policy.format + } + }; + const client: ServiceClient = await createGenericClient(context.root.credentials); + const policyString = await client.sendRequest({ + method: "PUT", + body: body, + // tslint:disable-next-line: no-non-null-assertion + url: `https://management.azure.com/subscriptions/${context.root.subscriptionId}/resourceGroups/${context.root.resourceGroupName}/providers/Microsoft.ApiManagement/service/${context.root.serviceName}/apis/${context.root.apiName}/policies/policy?api-version=2021-04-01-preview` + }); + // tslint:disable: no-unsafe-any + // tslint:disable-next-line: no-unnecessary-local-variable + const policyTemp = policyString.parsedBody; + return policyTemp.properties.value; + } + const policyResult = await context.root.client.apiPolicy.createOrUpdate(context.root.resourceGroupName, context.root.serviceName, context.root.apiName, policy); + return policyResult._response.bodyAsText; } public getDefaultPolicy() : string { diff --git a/src/extension.ts b/src/extension.ts index 966425c..e71bf02 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -12,6 +12,7 @@ import { addApiFilter } from './commands/addApiFilter'; import { addApiToGateway } from './commands/addApiToGateway'; import { addApiToProduct } from './commands/addApiToProduct'; import { copySubscriptionKey } from './commands/copySubscriptionKey'; +import { createGraphqlApi } from './commands/createGraphqlApi'; import { createService } from './commands/createService'; import { debugPolicy } from './commands/debugPolicies/debugPolicy'; import { deleteNode } from './commands/deleteNode'; @@ -21,6 +22,7 @@ import { generateFunctions } from './commands/generateFunctions'; import { generateNewGatewayToken } from './commands/generateNewGatewayToken'; import { importFunctionApp } from './commands/importFunctionApp/importFunctionApp'; import { importFunctionAppToApi } from './commands/importFunctionApp/importFunctionApp'; +import { importGraphqlSchemaByFile } from './commands/importGraphqlSchema'; import { importOpenApi } from './commands/importOpenApi'; import { importWebApp, importWebAppToApi } from './commands/importWebApp/importWebApp'; import { createNamedValue, updateNamedValue } from './commands/manageNamedValue'; @@ -31,6 +33,7 @@ import { openWorkingFolder } from './commands/openWorkingFolder'; import { revisions } from './commands/revisions'; import { setCustomHostName } from './commands/setCustomHostName'; import { setupWorkingFolder } from './commands/setupWorkingFolder'; +import { showGraphqlAPIQuery } from './commands/showGraphqlAPIQuery'; import { testOperation } from './commands/testOperation'; import { doubleClickDebounceDelay } from './constants'; import { activate } from './debugger/extension'; @@ -42,6 +45,7 @@ import { AzureAccountTreeItem } from './explorer/AzureAccountTreeItem'; import { ApiResourceEditor } from './explorer/editors/arm/ApiResourceEditor'; import { OperationResourceEditor } from './explorer/editors/arm/OperationResourceEditor'; import { ProductResourceEditor } from './explorer/editors/arm/ProductResourceEditor'; +import { GraphqlApiSchemaEditor } from './explorer/editors/graphqlApiSchema/GraphqlApiSchemaEditor'; import { OpenApiEditor } from './explorer/editors/openApi/OpenApiEditor'; import { ApiPolicyEditor } from './explorer/editors/policy/ApiPolicyEditor'; import { OperationPolicyEditor } from './explorer/editors/policy/OperationPolicyEditor'; @@ -50,6 +54,8 @@ import { ServicePolicyEditor } from './explorer/editors/policy/ServicePolicyEdit import { GatewayApisTreeItem } from './explorer/GatewayApisTreeItem'; import { GatewayApiTreeItem } from './explorer/GatewayApiTreeItem'; import { GatewayTreeItem } from './explorer/GatewayTreeItem'; +import { GraphqlApiTreeItem } from './explorer/GraphqlApiTreeItem'; +import { GraphqlObjectTypeTreeItem } from './explorer/GraphqlObjectTypeTreeItem'; import { NamedValuesTreeItem } from './explorer/NamedValuesTreeItem'; import { NamedValueTreeItem } from './explorer/NamedValueTreeItem'; import { OperationPolicyTreeItem } from './explorer/OperationPolicyTreeItem'; @@ -104,6 +110,7 @@ function registerCommands(tree: AzExtTreeDataProvider): void { registerCommand('azureApiManagement.testOperation', testOperation); registerCommand('azureApiManagement.importOpenApiByFile', async (context: IActionContext, node?: ApisTreeItem) => { await importOpenApi(context, node, false); }); registerCommand('azureApiManagement.importOpenApiByLink', async (context: IActionContext, node?: ApisTreeItem) => { await importOpenApi(context, node, true); }); + registerCommand('azureApiManagement.importGraphqlAPIByLink', async (context: IActionContext, node?: ApisTreeItem) => { await createGraphqlApi(context, node); }); registerCommand('azureApiManagement.createNamedValue', async (context: IActionContext, node?: NamedValuesTreeItem) => { await createNamedValue(context, node); }); registerCommand('azureApiManagement.deleteNamedValue', async (context: IActionContext, node?: AzureTreeItem) => await deleteNode(context, NamedValueTreeItem.contextValue, node)); registerCommand('azureApiManagement.updateNamedValue', updateNamedValue); @@ -123,6 +130,7 @@ function registerCommands(tree: AzExtTreeDataProvider): void { registerCommand('azureApiManagement.generateKubernetesDeployment', generateKubernetesDeployment); registerCommand('azureApiManagement.generateNewGatewayToken', generateNewGatewayToken); registerCommand('azureApiManagement.debugPolicy', debugPolicy); + registerCommand('azureApiManagement.importGraphqlSchemaByFile', importGraphqlSchemaByFile); registerCommand('azureApiManagement.openExtensionWorkspaceFolder', openWorkingFolder); registerCommand('azureApiManagement.initializeExtensionWorkspaceFolder', setupWorkingFolder); @@ -180,6 +188,21 @@ function registerEditors(context: vscode.ExtensionContext) : void { vscode.commands.executeCommand('setContext', 'isEditorEnabled', true); }, doubleClickDebounceDelay); + const graphqlApiSchemaEditor: GraphqlApiSchemaEditor = new GraphqlApiSchemaEditor(); + context.subscriptions.push(graphqlApiSchemaEditor); + registerEvent('azureApiManagement.graphqlApiSchemaEditor.onDidSaveTextDocument', + vscode.workspace.onDidSaveTextDocument, + async (actionContext: IActionContext, doc: vscode.TextDocument) => { + await graphqlApiSchemaEditor.onDidSaveTextDocument(actionContext, context.globalState, doc); + }); + registerCommand('azureApiManagement.showGraphqlSchema', async (actionContext: IActionContext, node?: GraphqlApiTreeItem) => { + if (!node) { + node = await ext.tree.showTreeItemPicker(GraphqlApiTreeItem.contextValue, actionContext); + } + await graphqlApiSchemaEditor.showEditor(node); + vscode.commands.executeCommand('setContext', 'isEditorEnabled', true); + }, doubleClickDebounceDelay); + const apiEditor: OpenApiEditor = new OpenApiEditor(); context.subscriptions.push(apiEditor); registerEvent('azureApiManagement.apiEditor.onDidSaveTextDocument', @@ -193,6 +216,8 @@ function registerEditors(context: vscode.ExtensionContext) : void { vscode.commands.executeCommand('setContext', 'isEditorEnabled', true); }, doubleClickDebounceDelay); + registerCommand('azureApiManagement.showGraphqlAPIQuery', async (actionContext: IActionContext, node?: GraphqlObjectTypeTreeItem) => await showGraphqlAPIQuery(actionContext, node), doubleClickDebounceDelay); + const servicePolicyEditor: ServicePolicyEditor = new ServicePolicyEditor(); context.subscriptions.push(servicePolicyEditor); registerEvent('azureApiManagement.servicePolicyEditor.onDidSaveTextDocument', diff --git a/src/test.http b/src/test.http new file mode 100644 index 0000000..116fc2c --- /dev/null +++ b/src/test.http @@ -0,0 +1,4 @@ + +POST https://alzasloneuap03.azure-api.net/graphql-api + +{ "query": "query($id: ID!) { ship(id: $id) { id, active } }", "variables": { "id": "GOMSTREE" } } diff --git a/src/utils/apiUtil.ts b/src/utils/apiUtil.ts index 97db1dd..1c6412f 100644 --- a/src/utils/apiUtil.ts +++ b/src/utils/apiUtil.ts @@ -25,6 +25,13 @@ export namespace apiUtil { })).trim(); } + export async function askPath(): Promise { + const pathPrompt: string = localize('', "Enter API Path."); + return (await ext.ui.showInputBox({ + prompt: pathPrompt + })).trim(); + } + export function genApiId(apiName: string): string { const identifier = displayNameToIdentifier(apiName); return `/apis/${identifier}`;