From 7340e912d40fe937a49bf6837cfe292018bd30f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristina=20Isabel=20Ca=C3=B1izales?= Date: Mon, 18 Nov 2024 10:06:52 -0300 Subject: [PATCH 1/7] feat: create apex action from selected method --- .../check-feature-request/lib/src/index.js | 152 +++-- .github/actions/new-issue/lib/src/index.js | 109 +-- .../actions/validate-issue/lib/src/index.js | 642 +++++++++--------- .../validate-issue/lib/src/nodeVersions.js | 21 +- package-lock.json | 7 + package.json | 1 + .../src/cli/commandExecutor.ts | 2 +- .../salesforcedx-vscode-apex/package.json | 20 + .../package.nls.ja.json | 1 + .../salesforcedx-vscode-apex/package.nls.json | 3 +- .../src/commands/apexActionController.ts | 125 ++++ .../src/commands/createApexAction.ts | 77 +++ .../src/commands/index.ts | 4 +- .../src/commands/metadataOrchestrator.ts | 28 + .../salesforcedx-vscode-apex/src/index.ts | 6 + 15 files changed, 744 insertions(+), 454 deletions(-) create mode 100644 packages/salesforcedx-vscode-apex/src/commands/apexActionController.ts create mode 100644 packages/salesforcedx-vscode-apex/src/commands/createApexAction.ts create mode 100644 packages/salesforcedx-vscode-apex/src/commands/metadataOrchestrator.ts diff --git a/.github/actions/check-feature-request/lib/src/index.js b/.github/actions/check-feature-request/lib/src/index.js index fb1524d324..7d1d5db286 100644 --- a/.github/actions/check-feature-request/lib/src/index.js +++ b/.github/actions/check-feature-request/lib/src/index.js @@ -1,83 +1,91 @@ -'use strict'; +"use strict"; /* * Copyright (c) 2024, salesforce.com, inc. * All rights reserved. * Licensed under the BSD 3-Clause license. * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ -Object.defineProperty(exports, '__esModule', { value: true }); -const core_1 = require('@actions/core'); -const github_1 = require('@actions/github'); +Object.defineProperty(exports, "__esModule", { value: true }); +const core_1 = require("@actions/core"); +const github_1 = require("@actions/github"); async function run() { - try { - const issue = github_1.context.payload.issue; - if (!issue) { - (0, core_1.setFailed)('github.context.payload.issue does not exist'); - return; + try { + const issue = github_1.context.payload.issue; + if (!issue) { + (0, core_1.setFailed)("github.context.payload.issue does not exist"); + return; + } + // Temporary check to prevent this action from running on old issues + // This will prevent noise on tickets already being investigated + // This can be removed once the action has been running for a while + const creationDate = new Date(issue.created_at); + const cutoffDate = new Date("2023-06-14T00:00:00Z"); + if (creationDate < cutoffDate) { + console.log("Issue was created before 6/14/2023, skipping"); + return; + } + // Create a GitHub client + const token = (0, core_1.getInput)("repo-token"); + const octokit = (0, github_1.getOctokit)(token); + // Get owner and repo from context + const owner = github_1.context.repo.owner; + const repo = github_1.context.repo.repo; + const issue_number = issue.number; + console.log("Issue URL:", issue.html_url); + const { body } = issue; + const { login: author } = issue.user; + const { data: comments } = await getAllComments(); + // For version checks, we only care about comments from the author + const authorComments = comments.filter((comment) => comment?.user?.login === author); + // Build an array of the issue body and all of the comment bodies + const bodies = [ + body, + ...authorComments.map((comment) => comment.body), + ].filter((body) => body !== undefined); + console.log('bodies = ' + JSON.stringify(bodies)); + console.log('bodies.length = ' + bodies.length); + const core = require('@actions/core'); + if (bodies[0] === null) { + core.setOutput("is_feature_request", "false"); + } + else { + const featureRequestRegex = /(feature\s*request)/ig; + // Search all bodies and get an array of all versions found (first capture group) + const featureRequests = bodies + .map((body) => [...body.matchAll(featureRequestRegex)].map((match) => match[1])) + .flat(); + if (featureRequests.length > 0) { + console.log('This issue is a feature request!'); + addLabel("type:enhancements"); + core.setOutput("is_feature_request", "true"); + } + else { + core.setOutput("is_feature_request", "false"); + } + } + // --------- + // FUNCTIONS + // --------- + async function getAllComments() { + return await octokit.rest.issues.listComments({ + owner, + repo, + issue_number, + }); + } + async function addLabel(label) { + await octokit.rest.issues.addLabels({ + owner, + repo, + issue_number, + labels: [label], + }); + } } - // Temporary check to prevent this action from running on old issues - // This will prevent noise on tickets already being investigated - // This can be removed once the action has been running for a while - const creationDate = new Date(issue.created_at); - const cutoffDate = new Date('2023-06-14T00:00:00Z'); - if (creationDate < cutoffDate) { - console.log('Issue was created before 6/14/2023, skipping'); - return; + catch (err) { + const error = err; + (0, core_1.setFailed)(error.message); } - // Create a GitHub client - const token = (0, core_1.getInput)('repo-token'); - const octokit = (0, github_1.getOctokit)(token); - // Get owner and repo from context - const owner = github_1.context.repo.owner; - const repo = github_1.context.repo.repo; - const issue_number = issue.number; - console.log('Issue URL:', issue.html_url); - const { body } = issue; - const { login: author } = issue.user; - const { data: comments } = await getAllComments(); - // For version checks, we only care about comments from the author - const authorComments = comments.filter(comment => comment?.user?.login === author); - // Build an array of the issue body and all of the comment bodies - const bodies = [body, ...authorComments.map(comment => comment.body)].filter(body => body !== undefined); - console.log('bodies = ' + JSON.stringify(bodies)); - console.log('bodies.length = ' + bodies.length); - const core = require('@actions/core'); - if (bodies[0] === null) { - core.setOutput('is_feature_request', 'false'); - } else { - const featureRequestRegex = /(feature\s*request)/gi; - // Search all bodies and get an array of all versions found (first capture group) - const featureRequests = bodies.map(body => [...body.matchAll(featureRequestRegex)].map(match => match[1])).flat(); - if (featureRequests.length > 0) { - console.log('This issue is a feature request!'); - addLabel('type:enhancements'); - core.setOutput('is_feature_request', 'true'); - } else { - core.setOutput('is_feature_request', 'false'); - } - } - // --------- - // FUNCTIONS - // --------- - async function getAllComments() { - return await octokit.rest.issues.listComments({ - owner, - repo, - issue_number - }); - } - async function addLabel(label) { - await octokit.rest.issues.addLabels({ - owner, - repo, - issue_number, - labels: [label] - }); - } - } catch (err) { - const error = err; - (0, core_1.setFailed)(error.message); - } } run(); -//# sourceMappingURL=index.js.map +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/.github/actions/new-issue/lib/src/index.js b/.github/actions/new-issue/lib/src/index.js index 05cc998a32..6e2c55bfb0 100644 --- a/.github/actions/new-issue/lib/src/index.js +++ b/.github/actions/new-issue/lib/src/index.js @@ -1,65 +1,66 @@ -'use strict'; +"use strict"; /* * Copyright (c) 2024, salesforce.com, inc. * All rights reserved. * Licensed under the BSD 3-Clause license. * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ -Object.defineProperty(exports, '__esModule', { value: true }); -const core_1 = require('@actions/core'); -const github_1 = require('@actions/github'); +Object.defineProperty(exports, "__esModule", { value: true }); +const core_1 = require("@actions/core"); +const github_1 = require("@actions/github"); async function run() { - try { - // The issue request exists on payload when an issue is created - // Sets action status to failed when issue does not exist on payload. - const issue = github_1.context.payload.issue; - if (!issue) { - (0, core_1.setFailed)('github.context.payload.issue does not exist'); - return; + try { + // The issue request exists on payload when an issue is created + // Sets action status to failed when issue does not exist on payload. + const issue = github_1.context.payload.issue; + if (!issue) { + (0, core_1.setFailed)("github.context.payload.issue does not exist"); + return; + } + // Get input parameters. + const token = (0, core_1.getInput)("repo-token"); + const message = (0, core_1.getInput)("message"); + const label = (0, core_1.getInput)("label"); + console.log("message: ", message); + console.log("label: ", label); + // Create a GitHub client. + const octokit = (0, github_1.getOctokit)(token); + // Get owner and repo from context + const owner = github_1.context.repo.owner; + const repo = github_1.context.repo.repo; + // Create a comment on Issue + // https://octokit.github.io/rest.js/#octokit-routes-issues-create-comment + console.log("owner: " + owner); + console.log("repo: " + repo); + console.log("issue number: " + issue.number); + const issueLabels = issue.labels; + console.log("issue labels: ", issueLabels); + const { data: comments } = await octokit.rest.issues.listComments({ + owner, + repo, + issue_number: issue.number, + }); + // If we have comments check out that this comment has not been previously commented + if (comments.length) { + if (comments.some((comment) => comment.body === message)) { + console.log("Already commented"); + return; + } + } + const response = await octokit.rest.issues.createComment({ + owner, + repo, + // eslint-disable-next-line @typescript-eslint/camelcase + issue_number: issue.number, + body: message, + }); + console.log("created comment URL: " + response.data.html_url); + (0, core_1.setOutput)("comment-url", response.data.html_url); } - // Get input parameters. - const token = (0, core_1.getInput)('repo-token'); - const message = (0, core_1.getInput)('message'); - const label = (0, core_1.getInput)('label'); - console.log('message: ', message); - console.log('label: ', label); - // Create a GitHub client. - const octokit = (0, github_1.getOctokit)(token); - // Get owner and repo from context - const owner = github_1.context.repo.owner; - const repo = github_1.context.repo.repo; - // Create a comment on Issue - // https://octokit.github.io/rest.js/#octokit-routes-issues-create-comment - console.log('owner: ' + owner); - console.log('repo: ' + repo); - console.log('issue number: ' + issue.number); - const issueLabels = issue.labels; - console.log('issue labels: ', issueLabels); - const { data: comments } = await octokit.rest.issues.listComments({ - owner, - repo, - issue_number: issue.number - }); - // If we have comments check out that this comment has not been previously commented - if (comments.length) { - if (comments.some(comment => comment.body === message)) { - console.log('Already commented'); - return; - } + catch (err) { + const error = err; + (0, core_1.setFailed)(error.message); } - const response = await octokit.rest.issues.createComment({ - owner, - repo, - // eslint-disable-next-line @typescript-eslint/camelcase - issue_number: issue.number, - body: message - }); - console.log('created comment URL: ' + response.data.html_url); - (0, core_1.setOutput)('comment-url', response.data.html_url); - } catch (err) { - const error = err; - (0, core_1.setFailed)(error.message); - } } run(); -//# sourceMappingURL=index.js.map +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/.github/actions/validate-issue/lib/src/index.js b/.github/actions/validate-issue/lib/src/index.js index 5a530661fe..61f9c73f67 100644 --- a/.github/actions/validate-issue/lib/src/index.js +++ b/.github/actions/validate-issue/lib/src/index.js @@ -1,338 +1,354 @@ -'use strict'; +"use strict"; /* * Copyright (c) 2024, salesforce.com, inc. * All rights reserved. * Licensed under the BSD 3-Clause license. * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ -Object.defineProperty(exports, '__esModule', { value: true }); -const core_1 = require('@actions/core'); -const github_1 = require('@actions/github'); -const child_process_1 = require('child_process'); -const semver = require('semver'); -const fs_1 = require('fs'); -const path = require('path'); -const nodeVersions_1 = require('./nodeVersions'); +Object.defineProperty(exports, "__esModule", { value: true }); +const core_1 = require("@actions/core"); +const github_1 = require("@actions/github"); +const child_process_1 = require("child_process"); +const semver = require("semver"); +const fs_1 = require("fs"); +const path = require("path"); +const nodeVersions_1 = require("./nodeVersions"); async function run() { - try { - const issue = github_1.context.payload.issue; - if (!issue) { - (0, core_1.setFailed)('github.context.payload.issue does not exist'); - return; - } - // Temporary check to prevent this action from running on old issues - // This will prevent noise on tickets already being investigated - // This can be removed once the action has been running for a while - const creationDate = new Date(issue.created_at); - const cutoffDate = new Date('2023-06-14T00:00:00Z'); - if (creationDate < cutoffDate) { - console.log('Issue was created before 6/14/2023, skipping'); - return; - } - // Create a GitHub client - const token = (0, core_1.getInput)('repo-token'); - const octokit = (0, github_1.getOctokit)(token); - // Get owner and repo from context - const owner = github_1.context.repo.owner; - const repo = github_1.context.repo.repo; - const issue_number = issue.number; - console.log('Issue URL:', issue.html_url); - const { body } = issue; - const { login: author } = issue.user; - const { data: comments } = await getAllComments(); - // For version checks, we only care about comments from the author - const authorComments = comments.filter(comment => comment?.user?.login === author); - // Build an array of the issue body and all of the comment bodies - const bodies = [body, ...authorComments.map(comment => comment.body)].filter(body => body !== undefined); - console.log('bodies = ' + JSON.stringify(bodies)); - console.log('bodies.length = ' + bodies.length); - if (bodies[0] === null) { - console.log('No content provided in issue body'); - const message = getFile('../../messages/provide-version.md', { - THE_AUTHOR: issue.user.login - }); - postComment(message); - addLabel('missing required information'); - } else { - let extensionsValid = true; - let vscodeValid = true; - let osVersionValid = true; - let cliValid = true; - let lastWorkingVersionValid = true; - let provideVersionAlreadyRequested = false; - // Checking Salesforce Extension Pack version - // The text "Salesforce Extension Version in VS Code" can be either bolded or unbolded - const extensionsVersionRegex = - /(?:\*{2}Salesforce Extension Version in VS Code\*{2}:\s*v?(\d{2}\.\d{1,2}\.\d))|(?:Salesforce Extension Version in VS Code:\s*v?(\d{2}\.\d{1,2}\.\d))/g; - // Search all bodies and get an array of all versions found (first or second capture group) - const extensionsVersions = bodies - .map(body => [...body.matchAll(extensionsVersionRegex)].map(match => match[1] || match[2])) - .flat(); - console.log('extensionsVersions', extensionsVersions); - if (extensionsVersions.length > 0) { - const extensionsLatest = getLatestExtensionsVersion(); - console.log('extensionsLatest', extensionsLatest); - const oneSatisfies = extensionsVersions.some(version => semver.gte(version, extensionsLatest)); - if (!oneSatisfies) { - const oldExtensions = getFile('../../messages/old-extensions.md', { - THE_AUTHOR: author, - USER_VERSION: extensionsVersions.join('`, `'), - LATEST_VERSION: extensionsLatest - }); - postComment(oldExtensions); - } - if (extensionsValid) { - console.log('A valid extensions version is provided!'); - } else { - console.log('The extensions version provided is NOT valid'); - addLabel('missing required information'); - } - } else { - console.log('Extensions version is NOT provided'); - if (!provideVersionAlreadyRequested) { - const message = getFile('../../messages/provide-version.md', { - THE_AUTHOR: issue.user.login - }); - postComment(message); - provideVersionAlreadyRequested = true; - addLabel('missing required information'); - } - extensionsValid = false; - } - // Checking VSCode version - const vscodeVersionRegex = - /(?:\*{2}VS Code version\*{2}:\s*(?:Version:\s*)?v?(1\.\d{2}\.\d))|(?:VS Code version:\s*(?:Version:\s*)?v?(1\.\d{2}\.\d))/g; - // Search all bodies and get an array of all versions found (first or second capture group) - const vscodeVersions = bodies - .map(body => [...body.matchAll(vscodeVersionRegex)].map(match => match[1] || match[2])) - .flat(); - console.log('vscodeVersions', vscodeVersions); - if (vscodeVersions.length > 0) { - const vscodeMinVersion = getMinimumVSCodeVersion(); - console.log('vscodeMinVersion', vscodeMinVersion); - const oneSatisfies = vscodeVersions.some(version => semver.gte(version, vscodeMinVersion)); - if (!oneSatisfies) { - const oldVSCode = getFile('../../messages/unsupported-vscode.md', { - THE_AUTHOR: author, - USER_VERSION: vscodeVersions.join('`, `'), - MIN_VERSION: vscodeMinVersion - }); - postComment(oldVSCode); - vscodeValid = false; + try { + const issue = github_1.context.payload.issue; + if (!issue) { + (0, core_1.setFailed)("github.context.payload.issue does not exist"); + return; } - if (vscodeValid) { - console.log('A valid VSCode version is provided!'); - } else { - console.log('The VSCode version provided is NOT valid'); - addLabel('missing required information'); + // Temporary check to prevent this action from running on old issues + // This will prevent noise on tickets already being investigated + // This can be removed once the action has been running for a while + const creationDate = new Date(issue.created_at); + const cutoffDate = new Date("2023-06-14T00:00:00Z"); + if (creationDate < cutoffDate) { + console.log("Issue was created before 6/14/2023, skipping"); + return; } - } else { - console.log('VSCode version is NOT provided'); - if (!provideVersionAlreadyRequested) { - const message = getFile('../../messages/provide-version.md', { - THE_AUTHOR: issue.user.login - }); - postComment(message); - provideVersionAlreadyRequested = true; - addLabel('missing required information'); + // Create a GitHub client + const token = (0, core_1.getInput)("repo-token"); + const octokit = (0, github_1.getOctokit)(token); + // Get owner and repo from context + const owner = github_1.context.repo.owner; + const repo = github_1.context.repo.repo; + const issue_number = issue.number; + console.log("Issue URL:", issue.html_url); + const { body } = issue; + const { login: author } = issue.user; + const { data: comments } = await getAllComments(); + // For version checks, we only care about comments from the author + const authorComments = comments.filter((comment) => comment?.user?.login === author); + // Build an array of the issue body and all of the comment bodies + const bodies = [ + body, + ...authorComments.map((comment) => comment.body), + ].filter((body) => body !== undefined); + console.log('bodies = ' + JSON.stringify(bodies)); + console.log('bodies.length = ' + bodies.length); + if (bodies[0] === null) { + console.log('No content provided in issue body'); + const message = getFile("../../messages/provide-version.md", { + THE_AUTHOR: issue.user.login, + }); + postComment(message); + addLabel("missing required information"); } - vscodeValid = false; - } - // Checking presence of OS and version - // NOTE: negative lookahead used in this regex due to false match when OS and version is blank - const osVersionRegex = - /(?:\*{2}OS and version\*{2}:\s*(?!\*\*VS|VS)\S.*?)(?=\r?\n|$)|(?:OS and version:\s*(?!\*\*VS|VS)\S.*?)(?=\r?\n|$)/g; - // Search all bodies and get an array of all versions found (first or second capture group) - const osVersions = bodies - .map(body => [...body.matchAll(osVersionRegex)].map(match => match[1] || match[2])) - .flat(); - if (osVersions.length > 0) { - console.log('OS and version is provided!'); - } else { - console.log('OS and version is NOT provided'); - if (!provideVersionAlreadyRequested) { - const message = getFile('../../messages/provide-version.md', { - THE_AUTHOR: issue.user.login - }); - postComment(message); - provideVersionAlreadyRequested = true; - addLabel('missing required information'); + else { + let extensionsValid = true; + let vscodeValid = true; + let osVersionValid = true; + let cliValid = true; + let lastWorkingVersionValid = true; + let provideVersionAlreadyRequested = false; + // Checking Salesforce Extension Pack version + // The text "Salesforce Extension Version in VS Code" can be either bolded or unbolded + const extensionsVersionRegex = /(?:\*{2}Salesforce Extension Version in VS Code\*{2}:\s*v?(\d{2}\.\d{1,2}\.\d))|(?:Salesforce Extension Version in VS Code:\s*v?(\d{2}\.\d{1,2}\.\d))/g; + // Search all bodies and get an array of all versions found (first or second capture group) + const extensionsVersions = bodies + .map((body) => [...body.matchAll(extensionsVersionRegex)].map((match) => match[1] || match[2])) + .flat(); + console.log('extensionsVersions', extensionsVersions); + if (extensionsVersions.length > 0) { + const extensionsLatest = getLatestExtensionsVersion(); + console.log('extensionsLatest', extensionsLatest); + const oneSatisfies = extensionsVersions.some((version) => semver.gte(version, extensionsLatest)); + if (!oneSatisfies) { + const oldExtensions = getFile("../../messages/old-extensions.md", { + THE_AUTHOR: author, + USER_VERSION: extensionsVersions.join("`, `"), + LATEST_VERSION: extensionsLatest + }); + postComment(oldExtensions); + } + if (extensionsValid) { + console.log("A valid extensions version is provided!"); + } + else { + console.log("The extensions version provided is NOT valid"); + addLabel("missing required information"); + } + } + else { + console.log("Extensions version is NOT provided"); + if (!provideVersionAlreadyRequested) { + const message = getFile("../../messages/provide-version.md", { + THE_AUTHOR: issue.user.login, + }); + postComment(message); + provideVersionAlreadyRequested = true; + addLabel("missing required information"); + } + extensionsValid = false; + } + // Checking VSCode version + const vscodeVersionRegex = /(?:\*{2}VS Code version\*{2}:\s*(?:Version:\s*)?v?(1\.\d{2}\.\d))|(?:VS Code version:\s*(?:Version:\s*)?v?(1\.\d{2}\.\d))/g; + // Search all bodies and get an array of all versions found (first or second capture group) + const vscodeVersions = bodies + .map((body) => [...body.matchAll(vscodeVersionRegex)].map((match) => match[1] || match[2])) + .flat(); + console.log('vscodeVersions', vscodeVersions); + if (vscodeVersions.length > 0) { + const vscodeMinVersion = getMinimumVSCodeVersion(); + console.log('vscodeMinVersion', vscodeMinVersion); + const oneSatisfies = vscodeVersions.some((version) => semver.gte(version, vscodeMinVersion)); + if (!oneSatisfies) { + const oldVSCode = getFile("../../messages/unsupported-vscode.md", { + THE_AUTHOR: author, + USER_VERSION: vscodeVersions.join("`, `"), + MIN_VERSION: vscodeMinVersion + }); + postComment(oldVSCode); + vscodeValid = false; + } + if (vscodeValid) { + console.log("A valid VSCode version is provided!"); + } + else { + console.log("The VSCode version provided is NOT valid"); + addLabel("missing required information"); + } + } + else { + console.log("VSCode version is NOT provided"); + if (!provideVersionAlreadyRequested) { + const message = getFile("../../messages/provide-version.md", { + THE_AUTHOR: issue.user.login, + }); + postComment(message); + provideVersionAlreadyRequested = true; + addLabel("missing required information"); + } + vscodeValid = false; + } + // Checking presence of OS and version + // NOTE: negative lookahead used in this regex due to false match when OS and version is blank + const osVersionRegex = /(?:\*{2}OS and version\*{2}:\s*(?!\*\*VS|VS)\S.*?)(?=\r?\n|$)|(?:OS and version:\s*(?!\*\*VS|VS)\S.*?)(?=\r?\n|$)/g; + // Search all bodies and get an array of all versions found (first or second capture group) + const osVersions = bodies + .map((body) => [...body.matchAll(osVersionRegex)].map((match) => match[1] || match[2])) + .flat(); + if (osVersions.length > 0) { + console.log("OS and version is provided!"); + } + else { + console.log("OS and version is NOT provided"); + if (!provideVersionAlreadyRequested) { + const message = getFile("../../messages/provide-version.md", { + THE_AUTHOR: issue.user.login, + }); + postComment(message); + provideVersionAlreadyRequested = true; + addLabel("missing required information"); + } + osVersionValid = false; + } + // Checking presence of last working extensions version + const lastWorkingVersionRegex = /(\*{2}Most recent version of the extensions where this was working\*{2}:\s*\S.*\r\n)|(Most recent version of the extensions where this was working:\s*\S.*\r\n)|(\*{2}Most recent version of the extensions where this was working\*{2}:\s*\S.*$)|(Most recent version of the extensions where this was working:\s*\S.*$)/g; + // Search all bodies and get an array of all versions found (first or second capture group) + const lastWorkingVersions = bodies + .map((body) => [...body.matchAll(lastWorkingVersionRegex)].map((match) => match[1] || match[2])) + .flat(); + if (lastWorkingVersions.length > 0) { + console.log("Last working version is provided!"); + } + else { + console.log("Last working version is NOT provided"); + if (!provideVersionAlreadyRequested) { + const message = getFile("../../messages/provide-version.md", { + THE_AUTHOR: issue.user.login, + }); + postComment(message); + provideVersionAlreadyRequested = true; + addLabel("missing required information"); + } + lastWorkingVersionValid = false; + } + // *** The below is the check for CLI version, code reused from CLI Team's repo *** + const sfVersionRegex = /(?:Salesforce CLI Version|(?:\*{2}Salesforce CLI Version\*{2})):\s*(?:@salesforce\/cli\/)?(\d+\.\d+\.\d+)/g; + const sfdxVersionRegex = /(?:Salesforce CLI Version|(?:\*{2}Salesforce CLI Version\*{2})):\s*(?:sfdx-cli\/)?(\d+\.\d+\.\d+)/g; + const nodeVersionRegex = /node-v(\d{2})\.\d+\.\d+/g; + // Search all bodies and get an array of all versions found (first capture group) + const sfVersions = bodies + .map((body) => [...body.matchAll(sfVersionRegex)].map((match) => match[1])) + .flat(); + const sfdxVersions = bodies + .map((body) => [...body.matchAll(sfdxVersionRegex)].map((match) => match[1])) + .flat(); + const nodeVersions = bodies + .map((body) => [...body.matchAll(nodeVersionRegex)].map((match) => match[1])) + .flat(); + console.log("sfVersions", sfVersions); + console.log("sfdxVersions", sfdxVersions); + console.log("nodeVersions", nodeVersions); + if ((sfVersions.length > 0 || sfdxVersions.length > 0)) { + if (sfVersions.length > 0) { + const oneSatisfies = sfVersions.some((version) => semver.gte(version, '2.0.0')); + if (!oneSatisfies) { + // If not, share deprecation information + const sfV1 = getFile("../../messages/deprecated-cli.md", { + THE_AUTHOR: author, + OLD_CLI: "`sf` (v1)", + }); + postComment(sfV1); + cliValid = false; + } + } + if (sfdxVersions.find((v) => v.startsWith("7.")) && + !sfVersions.find((v) => v.startsWith("2."))) { + const noOldSfdx = getFile("../../messages/deprecated-cli.md", { + THE_AUTHOR: author, + OLD_CLI: "`sfdx` (v7)", + }); + postComment(noOldSfdx); + cliValid = false; + } + if (nodeVersions.length > 0) { + if (!(await (0, nodeVersions_1.isAnyVersionValid)(new Date())(nodeVersions))) { + const nodeVersionMessage = getFile("../../messages/unsupported-node.md", { + THE_AUTHOR: author, + NODE_VERSION: nodeVersions.join("`, `"), + }); + postComment(nodeVersionMessage); + closeIssue(); + cliValid = false; + } + } + if (cliValid) { + console.log("A valid CLI version is provided!"); + } + else { + console.log("Information provided is NOT valid"); + addLabel("missing required information"); + } + } + else { + console.log("Full version information was not provided"); + if (!provideVersionAlreadyRequested) { + const message = getFile("../../messages/provide-version.md", { + THE_AUTHOR: issue.user.login, + }); + postComment(message); + provideVersionAlreadyRequested = true; + addLabel("missing required information"); + } + cliValid = false; + } + if (extensionsValid && vscodeValid && osVersionValid && cliValid && lastWorkingVersionValid) { + addLabel("validated"); + removeLabel("missing required information"); + } + else { + console.log("You have one or more missing/invalid versions."); + addLabel("missing required information"); + } } - osVersionValid = false; - } - // Checking presence of last working extensions version - const lastWorkingVersionRegex = - /(\*{2}Most recent version of the extensions where this was working\*{2}:\s*\S.*\r\n)|(Most recent version of the extensions where this was working:\s*\S.*\r\n)|(\*{2}Most recent version of the extensions where this was working\*{2}:\s*\S.*$)|(Most recent version of the extensions where this was working:\s*\S.*$)/g; - // Search all bodies and get an array of all versions found (first or second capture group) - const lastWorkingVersions = bodies - .map(body => [...body.matchAll(lastWorkingVersionRegex)].map(match => match[1] || match[2])) - .flat(); - if (lastWorkingVersions.length > 0) { - console.log('Last working version is provided!'); - } else { - console.log('Last working version is NOT provided'); - if (!provideVersionAlreadyRequested) { - const message = getFile('../../messages/provide-version.md', { - THE_AUTHOR: issue.user.login - }); - postComment(message); - provideVersionAlreadyRequested = true; - addLabel('missing required information'); + // --------- + // FUNCTIONS + // --------- + async function closeIssue() { + return await octokit.rest.issues.update({ + owner, + repo, + issue_number, + state: "closed", + }); } - lastWorkingVersionValid = false; - } - // *** The below is the check for CLI version, code reused from CLI Team's repo *** - const sfVersionRegex = - /(?:Salesforce CLI Version|(?:\*{2}Salesforce CLI Version\*{2})):\s*(?:@salesforce\/cli\/)?(\d+\.\d+\.\d+)/g; - const sfdxVersionRegex = - /(?:Salesforce CLI Version|(?:\*{2}Salesforce CLI Version\*{2})):\s*(?:sfdx-cli\/)?(\d+\.\d+\.\d+)/g; - const nodeVersionRegex = /node-v(\d{2})\.\d+\.\d+/g; - // Search all bodies and get an array of all versions found (first capture group) - const sfVersions = bodies.map(body => [...body.matchAll(sfVersionRegex)].map(match => match[1])).flat(); - const sfdxVersions = bodies.map(body => [...body.matchAll(sfdxVersionRegex)].map(match => match[1])).flat(); - const nodeVersions = bodies.map(body => [...body.matchAll(nodeVersionRegex)].map(match => match[1])).flat(); - console.log('sfVersions', sfVersions); - console.log('sfdxVersions', sfdxVersions); - console.log('nodeVersions', nodeVersions); - if (sfVersions.length > 0 || sfdxVersions.length > 0) { - if (sfVersions.length > 0) { - const oneSatisfies = sfVersions.some(version => semver.gte(version, '2.0.0')); - if (!oneSatisfies) { - // If not, share deprecation information - const sfV1 = getFile('../../messages/deprecated-cli.md', { - THE_AUTHOR: author, - OLD_CLI: '`sf` (v1)' + async function getAllComments() { + return await octokit.rest.issues.listComments({ + owner, + repo, + issue_number, }); - postComment(sfV1); - cliValid = false; - } } - if (sfdxVersions.find(v => v.startsWith('7.')) && !sfVersions.find(v => v.startsWith('2.'))) { - const noOldSfdx = getFile('../../messages/deprecated-cli.md', { - THE_AUTHOR: author, - OLD_CLI: '`sfdx` (v7)' - }); - postComment(noOldSfdx); - cliValid = false; + async function postComment(body) { + // Check that this comment has not been previously commented + if (comments.length) { + if (comments.some((comment) => comment.body === body)) { + console.log("Already commented"); + return; + } + } + return await octokit.rest.issues.createComment({ + owner, + repo, + issue_number, + body, + }); } - if (nodeVersions.length > 0) { - if (!(await (0, nodeVersions_1.isAnyVersionValid)(new Date())(nodeVersions))) { - const nodeVersionMessage = getFile('../../messages/unsupported-node.md', { - THE_AUTHOR: author, - NODE_VERSION: nodeVersions.join('`, `') + async function addLabel(label) { + await octokit.rest.issues.addLabels({ + owner, + repo, + issue_number, + labels: [label], }); - postComment(nodeVersionMessage); - closeIssue(); - cliValid = false; - } } - if (cliValid) { - console.log('A valid CLI version is provided!'); - } else { - console.log('Information provided is NOT valid'); - addLabel('missing required information'); + async function removeLabel(label) { + try { + await octokit.rest.issues.removeLabel({ + owner, + repo, + issue_number, + name: label, + }); + } + catch (err) { + const error = err; + if (error.status === 404) { + console.log(`Cannot remove label '${label}' since it was not applied`); + return; + } + throw error; + } } - } else { - console.log('Full version information was not provided'); - if (!provideVersionAlreadyRequested) { - const message = getFile('../../messages/provide-version.md', { - THE_AUTHOR: issue.user.login - }); - postComment(message); - provideVersionAlreadyRequested = true; - addLabel('missing required information'); + function getLatestExtensionsVersion() { + const result = (0, child_process_1.execSync)(`npx vsce show salesforce.salesforcedx-vscode --json`).toString(); + return JSON.parse(result).versions[0].version; } - cliValid = false; - } - if (extensionsValid && vscodeValid && osVersionValid && cliValid && lastWorkingVersionValid) { - addLabel('validated'); - removeLabel('missing required information'); - } else { - console.log('You have one or more missing/invalid versions.'); - addLabel('missing required information'); - } - } - // --------- - // FUNCTIONS - // --------- - async function closeIssue() { - return await octokit.rest.issues.update({ - owner, - repo, - issue_number, - state: 'closed' - }); - } - async function getAllComments() { - return await octokit.rest.issues.listComments({ - owner, - repo, - issue_number - }); - } - async function postComment(body) { - // Check that this comment has not been previously commented - if (comments.length) { - if (comments.some(comment => comment.body === body)) { - console.log('Already commented'); - return; + function getMinimumVSCodeVersion() { + const currentDirectory = (0, child_process_1.execSync)(`pwd`).toString(); + // currentDirectory contains a newline at the end + const packageJsonDirectory = currentDirectory.slice(0, -1) + "/packages/salesforcedx-vscode-core/package.json"; + const packageJsonContent = (0, fs_1.readFileSync)(packageJsonDirectory, 'utf8'); + // The VSCode version has a carat in front that needs to be removed + return JSON.parse(packageJsonContent).engines.vscode.substring(1); } - } - return await octokit.rest.issues.createComment({ - owner, - repo, - issue_number, - body - }); - } - async function addLabel(label) { - await octokit.rest.issues.addLabels({ - owner, - repo, - issue_number, - labels: [label] - }); - } - async function removeLabel(label) { - try { - await octokit.rest.issues.removeLabel({ - owner, - repo, - issue_number, - name: label - }); - } catch (err) { - const error = err; - if (error.status === 404) { - console.log(`Cannot remove label '${label}' since it was not applied`); - return; + function getFile(filename, replacements) { + let contents = (0, fs_1.readFileSync)(path.join(__dirname, filename), "utf8"); + Object.entries(replacements || {}).map(([key, value]) => { + contents = contents.replaceAll(key, value); + }); + return contents; } - throw error; - } - } - function getLatestExtensionsVersion() { - const result = (0, child_process_1.execSync)(`npx vsce show salesforce.salesforcedx-vscode --json`).toString(); - return JSON.parse(result).versions[0].version; } - function getMinimumVSCodeVersion() { - const currentDirectory = (0, child_process_1.execSync)(`pwd`).toString(); - // currentDirectory contains a newline at the end - const packageJsonDirectory = currentDirectory.slice(0, -1) + '/packages/salesforcedx-vscode-core/package.json'; - const packageJsonContent = (0, fs_1.readFileSync)(packageJsonDirectory, 'utf8'); - // The VSCode version has a carat in front that needs to be removed - return JSON.parse(packageJsonContent).engines.vscode.substring(1); - } - function getFile(filename, replacements) { - let contents = (0, fs_1.readFileSync)(path.join(__dirname, filename), 'utf8'); - Object.entries(replacements || {}).map(([key, value]) => { - contents = contents.replaceAll(key, value); - }); - return contents; + catch (err) { + const error = err; + (0, core_1.setFailed)(error.message); } - } catch (err) { - const error = err; - (0, core_1.setFailed)(error.message); - } } run(); -//# sourceMappingURL=index.js.map +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/.github/actions/validate-issue/lib/src/nodeVersions.js b/.github/actions/validate-issue/lib/src/nodeVersions.js index dacd3c2c58..ee201e7226 100644 --- a/.github/actions/validate-issue/lib/src/nodeVersions.js +++ b/.github/actions/validate-issue/lib/src/nodeVersions.js @@ -1,16 +1,13 @@ -'use strict'; -Object.defineProperty(exports, '__esModule', { value: true }); +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); exports.isAnyVersionValid = void 0; -const isAnyVersionValid = currentDate => async versions => { - const resp = await (await fetch('https://raw.githubusercontent.com/nodejs/Release/main/schedule.json')).json(); - return versions - .map(version => `v${version}`) - .some( - formattedVersion => - formattedVersion in resp && +const isAnyVersionValid = (currentDate) => async (versions) => { + const resp = (await (await fetch("https://raw.githubusercontent.com/nodejs/Release/main/schedule.json")).json()); + return versions + .map((version) => `v${version}`) + .some((formattedVersion) => formattedVersion in resp && currentDate >= new Date(resp[formattedVersion].start) && - currentDate <= new Date(resp[formattedVersion].end) - ); + currentDate <= new Date(resp[formattedVersion].end)); }; exports.isAnyVersionValid = isAnyVersionValid; -//# sourceMappingURL=nodeVersions.js.map +//# sourceMappingURL=nodeVersions.js.map \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 31d5ea225a..bb8dc8706a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -59,6 +59,7 @@ "markdown-link-check": "^3.9.3", "ncp": "^2.0.0", "nyc": "15.1.0", + "openapi-types": "12.1.3", "ovsx": "0.8.0", "prettier": "3.3.3", "shelljs": "0.8.5", @@ -26406,6 +26407,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/openapi-types": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", + "dev": true + }, "node_modules/optionator": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", diff --git a/package.json b/package.json index 418cb1d095..d0b4e43fe1 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "markdown-link-check": "^3.9.3", "ncp": "^2.0.0", "nyc": "15.1.0", + "openapi-types": "12.1.3", "ovsx": "0.8.0", "prettier": "3.3.3", "shelljs": "0.8.5", diff --git a/packages/salesforcedx-utils-vscode/src/cli/commandExecutor.ts b/packages/salesforcedx-utils-vscode/src/cli/commandExecutor.ts index ca70a47004..834f34ce35 100644 --- a/packages/salesforcedx-utils-vscode/src/cli/commandExecutor.ts +++ b/packages/salesforcedx-utils-vscode/src/cli/commandExecutor.ts @@ -87,7 +87,7 @@ export class CompositeCliCommandExecutor { * Represents a command execution (a process has already been spawned for it). * This is tightly coupled with the execution model (child_process). * If we ever use a different executor, this class should be refactored and abstracted - * to take an event emitter/observable instead of child_proces. + * to take an event emitter/observable instead of child_process. */ export type CommandExecution = { readonly command: Command; diff --git a/packages/salesforcedx-vscode-apex/package.json b/packages/salesforcedx-vscode-apex/package.json index 6c2ff93165..8121e514d6 100644 --- a/packages/salesforcedx-vscode-apex/package.json +++ b/packages/salesforcedx-vscode-apex/package.json @@ -126,6 +126,18 @@ ] }, "menus": { + "editor/context": [ + { + "command": "sf.create.apex.action.method", + "when": "sf:project_opened && sf:has_target_org && resource =~ /.\\.(cls)?$/" + } + ], + "explorer/context": [ + { + "command": "sf.create.apex.action.method", + "when": "sf:project_opened && sf:has_target_org && resource =~ /.\\.(cls)?$/" + } + ], "view/title": [ { "command": "sf.test.view.run", @@ -235,6 +247,10 @@ { "command": "sf.test.view.collapseAll", "when": "sf:project_opened" + }, + { + "command": "sf.create.apex.action.method", + "when": "sf:project_opened && sf:has_target_org" } ] }, @@ -338,6 +354,10 @@ { "command": "sf.apex.test.last.method.run", "title": "%apex_test_last_method_run_text%" + }, + { + "command": "sf.create.apex.action.method", + "title": "%create_apex_action_method%" } ], "configuration": { diff --git a/packages/salesforcedx-vscode-apex/package.nls.ja.json b/packages/salesforcedx-vscode-apex/package.nls.ja.json index 4e2d97476e..caac5f9cb5 100644 --- a/packages/salesforcedx-vscode-apex/package.nls.ja.json +++ b/packages/salesforcedx-vscode-apex/package.nls.ja.json @@ -19,6 +19,7 @@ "apex_verbose_level_trace_description": "Output everything, including details about notifications and responses received by the client, and requests sent by the server.", "configuration_title": "Salesforce Apex Configuration", "collapse_tests_title": "SFDX: Apex テストを隠す", + "create_apex_action_method": "SFDX: Create Apex Action from Selected Method", "go_to_definition_title": "定義に移動", "java_home_description": "Specifies the folder path to the Java 11, Java 17, or Java 21 runtime used to launch the Apex Language Server. Note on Windows the backslashes must be escaped.\n\nMac Example: `/Library/Java/JavaVirtualMachines/openjdk-11.jdk/Contents/Home`\n\nWindows Example: `C:\\\\Program Files\\\\Zulu\\\\zulu-17`\n\nLinux Example: `/usr/lib/jvm/java-21-openjdk-amd64`", "refresh_test_title": "テストを更新", diff --git a/packages/salesforcedx-vscode-apex/package.nls.json b/packages/salesforcedx-vscode-apex/package.nls.json index f0adbccdb1..1b1f13a139 100644 --- a/packages/salesforcedx-vscode-apex/package.nls.json +++ b/packages/salesforcedx-vscode-apex/package.nls.json @@ -7,9 +7,9 @@ "apex_messages_level_trace_description": "Only output high-level messages of notifications and responses received by the client, and requests sent by the server.", "apex_off_level_trace_description": "Don't generate any output. Turn off all tracing.", "apex_semantic_errors_description": "Allow Apex Language Server to surface semantic errors", + "apex_test_last_method_run_text": "SFDX: Re-Run Last Run Apex Test Method", "apex_test_class_run_text": "SFDX: Run Apex Test Class", "apex_test_last_class_run_text": "SFDX: Re-Run Last Run Apex Test Class", - "apex_test_last_method_run_text": "SFDX: Re-Run Last Run Apex Test Method", "apex_test_method_run_text": "SFDX: Run Apex Test Method", "apex_test_run_text": "SFDX: Run Apex Tests", "apex_test_suite_build_text": "SFDX: Add Tests to Apex Test Suite", @@ -19,6 +19,7 @@ "apex_verbose_level_trace_description": "Output everything, including details about notifications and responses received by the client, and requests sent by the server.", "configuration_title": "Salesforce Apex Configuration", "collapse_tests_title": "SFDX: Collapse All Apex Tests", + "create_apex_action_method": "SFDX: Create Apex Action from Selected Method", "go_to_definition_title": "Go to Definition", "java_home_description": "Specifies the folder path to the Java 11, Java 17, or Java 21 runtime used to launch the Apex Language Server. Note on Windows the backslashes must be escaped.\n\nMac Example: `/Library/Java/JavaVirtualMachines/openjdk-11.jdk/Contents/Home`\n\nWindows Example: `C:\\\\Program Files\\\\Zulu\\\\zulu-17`\n\nLinux Example: `/usr/lib/jvm/java-21-openjdk-amd64`", "java_memory_description": "Specifies the amount of memory allocation to the Apex Language Server in MB, or null to use the system default value.", diff --git a/packages/salesforcedx-vscode-apex/src/commands/apexActionController.ts b/packages/salesforcedx-vscode-apex/src/commands/apexActionController.ts new file mode 100644 index 0000000000..3cb3e3cef3 --- /dev/null +++ b/packages/salesforcedx-vscode-apex/src/commands/apexActionController.ts @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2024, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ +import { Progress } from '@salesforce/apex-node-bundle'; +import { notificationService, workspaceUtils } from '@salesforce/salesforcedx-utils-vscode'; +import * as fs from 'fs'; +import { OpenAPIV3 } from 'openapi-types'; // Adjust the import path as necessary +import * as path from 'path'; +import { QuickPickItem } from 'vscode'; +import * as vscode from 'vscode'; +import { nls } from '../messages'; +import { getTelemetryService } from '../telemetry/telemetry'; +import { MetadataOrchestrator } from './metadataOrchestrator'; + +export class ApexActionController { + constructor( + private metadataOrchestrator: MetadataOrchestrator, // Dependency Injection + private progress?: Progress<{ + message?: string | undefined; + increment?: number | undefined; + }> + ) {} + public listApexMethods = (apexClassPath: string): Promise => { + // Read the content of the Apex class file + const fileContent = fs.readFileSync(apexClassPath).toString(); + + // Regular expression to match method declarations in Apex + const methodRegExp = /@[\w]+\s*\b(public|private|protected|global)\s+(static\s+)?[\w<>\[\]]+\s+(\w+)\s*\(/g; + + const methods: QuickPickItem[] = []; + let match; + + // Extract all method names that match the regular expression + while ((match = methodRegExp.exec(fileContent)) !== null) { + const methodName = match[3]; + methods.push({ + label: methodName, + description: apexClassPath + }); + } + + // Sort the methods alphabetically by name + methods.sort((a, b) => a.label.localeCompare(b.label)); + + return Promise.resolve(methods); + }; + + public createApexActionFromMethod = async (methodIdentifier: string): Promise => { + const telemetryService = await getTelemetryService(); + const progressReporter: Progress = { + report: value => { + if (value.type === 'StreamingClientProgress' || value.type === 'FormatTestResultProgress') { + this.progress?.report({ message: value.message }); + } + } + }; + try { + // Step 1: Validate Method + if (!this.isMethodEligible(methodIdentifier)) { + void notificationService.showErrorMessage( + '`Method ${methodIdentifier} is not eligible for Apex Action creation.`' + ); + throw new Error(`Method ${methodIdentifier} is not eligible for Apex Action creation.`); + } + + // Step 2: Extract Metadata + progressReporter.report({ message: 'Extracting metadata.' }); + const metadata = await this.metadataOrchestrator.extractMethodMetadata(methodIdentifier); + + // Step 3: Generate OpenAPI Document + progressReporter.report({ message: 'Generating OpenAPI document.' }); + const openApiDocument = this.generateOpenAPIDocument(metadata); + + // Step 4: Write OpenAPI Document to File + const openApiFilePath = `${methodIdentifier}_openapi.json`; + await this.saveDocument(openApiFilePath, openApiDocument); + + // Step 6: Notify Success + notificationService.showInformationMessage(`Apex Action created for method: ${methodIdentifier}`); + telemetryService.sendEventData('ApexActionCreated', { method: methodIdentifier }); + } catch (error) { + // Error Handling + notificationService.showErrorMessage(`Failed to create Apex Action: ${error.message}`); + telemetryService.sendException('ApexActionCreationFailed', error); + throw error; + } + }; + + public isMethodEligible = (methodIdentifier: string): boolean => { + // Placeholder for eligibility logic + return true; + }; + + private saveDocument = async (fileName: string, content: any): Promise => { + const openAPIdocumentsPath = path.join(workspaceUtils.getRootWorkspacePath(), 'OpenAPIdocuments'); + if (!fs.existsSync(openAPIdocumentsPath)) { + fs.mkdirSync(openAPIdocumentsPath); + } + const saveLocation = path.join(openAPIdocumentsPath, fileName); + fs.writeFileSync(saveLocation, JSON.stringify(content)); + await vscode.workspace.openTextDocument(saveLocation).then((newDocument: any) => { + void vscode.window.showTextDocument(newDocument); + }); + }; + + public generateOpenAPIDocument = (metadata: any): OpenAPIV3.Document => { + // Placeholder for OpenAPI generation logic + return { + openapi: '3.0.0', + info: { title: 'Apex Actions', version: '1.0.0' }, + paths: { + [`/apex/${metadata}`]: { + post: { + summary: `Invoke ${metadata}`, + operationId: metadata, + responses: { 200: { description: 'Success' } } + } + } + } + }; + }; +} diff --git a/packages/salesforcedx-vscode-apex/src/commands/createApexAction.ts b/packages/salesforcedx-vscode-apex/src/commands/createApexAction.ts new file mode 100644 index 0000000000..5848886c6f --- /dev/null +++ b/packages/salesforcedx-vscode-apex/src/commands/createApexAction.ts @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2024, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ +import { notificationService } from '@salesforce/salesforcedx-utils-vscode'; +import { readFileSync } from 'fs'; +import * as vscode from 'vscode'; +import { ApexActionController } from './apexActionController'; +import { MetadataOrchestrator } from './metadataOrchestrator'; + +const metadataOrchestrator = new MetadataOrchestrator(); +const controller = new ApexActionController(metadataOrchestrator); + +const validateAuraEnabledMethod = async ( + filePath: string, + cursorPosition: vscode.Position, + selectedMethod: string +): Promise => { + const lineNumber = cursorPosition.line; + // Read the content of the Apex class file + const fileContent = readFileSync(filePath, 'utf-8'); + const lines = fileContent.split('\n'); + + // Start from the current line and search upward for the method declaration + for (let i = lineNumber; i >= 0; i--) { + const line = lines[i].trim(); + + if (line.includes(selectedMethod)) continue; + // Check if the line contains @AuraEnabled + if (line.includes('@AuraEnabled')) { + return; + } + + // Check if the line contains a method declaration (regex matches methods) + const methodRegex = /\b(public|private|protected|global)\s+(static\s+)?[\w<>\[\]]+\s+\w+\s*\(/; + if (methodRegex.test(line)) { + notificationService.showWarningMessage(`The method "${selectedMethod}" is NOT annotated with @AuraEnabled.`); + throw Error(`The method "${selectedMethod}" is NOT annotated with @AuraEnabled.`); + } + } +}; + +export const createApexActionFromMethod = async (methodIdentifier: any): Promise => { + // Step 1: Prompt User to Select a Method + // const selectedMethod = await controller.listApexMethods(); + const editor = vscode.window.activeTextEditor; + if (!editor) { + notificationService.showErrorMessage('No active editor detected'); + throw Error('No active editor detected'); + } + + const document = editor.document; + let selectedMethod; + const cursorPosition = editor.selection.active; // Get cursor position + const lineText = document.lineAt(cursorPosition.line).text.trim(); // Get the line content + + // Regular expression to match a method declaration and extract its name + const methodRegex = /\b(public|private|protected|global)\s+(static\s+)?[\w<>\[\]]+\s+(\w+)\s*\(/; + + const match = methodRegex.exec(lineText); + if (match) { + selectedMethod = match[3]; // The third capture group is the method name + } + + if (!selectedMethod) { + notificationService.showErrorMessage('No method selected'); + return; + } + + const filePath = methodIdentifier.path; + await validateAuraEnabledMethod(filePath, cursorPosition, selectedMethod); + + // Step 2: Call Controller + await controller.createApexActionFromMethod(selectedMethod); +}; diff --git a/packages/salesforcedx-vscode-apex/src/commands/index.ts b/packages/salesforcedx-vscode-apex/src/commands/index.ts index e5676db8a8..1bd040a42e 100644 --- a/packages/salesforcedx-vscode-apex/src/commands/index.ts +++ b/packages/salesforcedx-vscode-apex/src/commands/index.ts @@ -5,16 +5,18 @@ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ export { anonApexDebug, anonApexExecute } from './anonApexExecute'; +export { ApexActionController } from './apexActionController'; export { apexLogGet } from './apexLogGet'; export { apexTestRun } from './apexTestRun'; export { - ApexLibraryTestRunExecutor, apexDebugClassRunCodeActionDelegate, apexDebugMethodRunCodeActionDelegate, + ApexLibraryTestRunExecutor, apexTestClassRunCodeAction, apexTestClassRunCodeActionDelegate, apexTestMethodRunCodeAction, apexTestMethodRunCodeActionDelegate } from './apexTestRunCodeAction'; export { apexTestSuiteAdd, apexTestSuiteCreate, apexTestSuiteRun } from './apexTestSuite'; +export { createApexActionFromMethod } from './createApexAction'; export { launchApexReplayDebuggerWithCurrentFile } from './launchApexReplayDebuggerWithCurrentFile'; diff --git a/packages/salesforcedx-vscode-apex/src/commands/metadataOrchestrator.ts b/packages/salesforcedx-vscode-apex/src/commands/metadataOrchestrator.ts new file mode 100644 index 0000000000..36de850bff --- /dev/null +++ b/packages/salesforcedx-vscode-apex/src/commands/metadataOrchestrator.ts @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2024, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ +export class MetadataOrchestrator { + constructor() { + // Initialization code here + } + + public async orchestrate(): Promise { + try { + // Orchestration logic here + } catch (error) { + console.error('Error during orchestration:', error); + } + } + public async extractMethodMetadata(methodIdentifier: any): Promise { + try { + // Logic to extract method metadata here + return methodIdentifier; + } catch (error) { + console.error('Error extracting method metadata:', error); + } + return undefined; + } +} diff --git a/packages/salesforcedx-vscode-apex/src/index.ts b/packages/salesforcedx-vscode-apex/src/index.ts index 457ac3d69a..ee6f7fd6e7 100644 --- a/packages/salesforcedx-vscode-apex/src/index.ts +++ b/packages/salesforcedx-vscode-apex/src/index.ts @@ -26,6 +26,7 @@ import { apexTestSuiteAdd, apexTestSuiteCreate, apexTestSuiteRun, + createApexActionFromMethod, launchApexReplayDebuggerWithCurrentFile } from './commands'; import { API, SET_JAVA_DOC_LINK } from './constants'; @@ -153,6 +154,10 @@ const registerCommands = (): vscode.Disposable => { 'sf.anon.apex.execute.selection', anonApexExecute ); + const createApexActionFromMethodCmd = vscode.commands.registerCommand( + 'sf.create.apex.action.method', + createApexActionFromMethod + ); const launchApexReplayDebuggerWithCurrentFileCmd = vscode.commands.registerCommand( 'sf.launch.apex.replay.debugger.with.current.file', launchApexReplayDebuggerWithCurrentFile @@ -178,6 +183,7 @@ const registerCommands = (): vscode.Disposable => { apexTestSuiteCreateCmd, apexTestSuiteRunCmd, apexTestSuiteAddCmd, + createApexActionFromMethodCmd, launchApexReplayDebuggerWithCurrentFileCmd ); }; From 805e93942884f9e4e2ae1b0fa7d14537e891f79e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristina=20Isabel=20Ca=C3=B1izales?= Date: Mon, 18 Nov 2024 17:29:28 -0300 Subject: [PATCH 2/7] chore: refactor --- package-lock.json | 12 +- package.json | 3 +- .../src/commands/apexActionController.ts | 95 +++++++-------- .../src/commands/createApexAction.ts | 67 +---------- .../src/commands/metadataOrchestrator.ts | 112 +++++++++++++++--- 5 files changed, 159 insertions(+), 130 deletions(-) diff --git a/package-lock.json b/package-lock.json index bb8dc8706a..79a953cdfe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -68,7 +68,8 @@ "ts-jest": "^29.1.1", "ts-loader": "^9.3.0", "ts-node": "10.9.2", - "typescript": "^5.6.2" + "typescript": "^5.6.2", + "yaml": "2.6.0" }, "engines": { "node": ">=20.9.0" @@ -32487,10 +32488,13 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" }, "node_modules/yaml": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz", - "integrity": "sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.0.tgz", + "integrity": "sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==", "dev": true, + "bin": { + "yaml": "bin.mjs" + }, "engines": { "node": ">= 14" } diff --git a/package.json b/package.json index d0b4e43fe1..27c16e4997 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,8 @@ "ts-jest": "^29.1.1", "ts-loader": "^9.3.0", "ts-node": "10.9.2", - "typescript": "^5.6.2" + "typescript": "^5.6.2", + "yaml": "2.6.0" }, "scripts": { "postinstall": "npm run bootstrap && npm run reformat && npm run check:peer-deps && npm run check:typescript-project-references", diff --git a/packages/salesforcedx-vscode-apex/src/commands/apexActionController.ts b/packages/salesforcedx-vscode-apex/src/commands/apexActionController.ts index 3cb3e3cef3..48c56396cc 100644 --- a/packages/salesforcedx-vscode-apex/src/commands/apexActionController.ts +++ b/packages/salesforcedx-vscode-apex/src/commands/apexActionController.ts @@ -9,11 +9,10 @@ import { notificationService, workspaceUtils } from '@salesforce/salesforcedx-ut import * as fs from 'fs'; import { OpenAPIV3 } from 'openapi-types'; // Adjust the import path as necessary import * as path from 'path'; -import { QuickPickItem } from 'vscode'; import * as vscode from 'vscode'; -import { nls } from '../messages'; +import { stringify } from 'yaml'; import { getTelemetryService } from '../telemetry/telemetry'; -import { MetadataOrchestrator } from './metadataOrchestrator'; +import { MetadataOrchestrator, MethodMetadata } from './metadataOrchestrator'; export class ApexActionController { constructor( @@ -23,32 +22,8 @@ export class ApexActionController { increment?: number | undefined; }> ) {} - public listApexMethods = (apexClassPath: string): Promise => { - // Read the content of the Apex class file - const fileContent = fs.readFileSync(apexClassPath).toString(); - // Regular expression to match method declarations in Apex - const methodRegExp = /@[\w]+\s*\b(public|private|protected|global)\s+(static\s+)?[\w<>\[\]]+\s+(\w+)\s*\(/g; - - const methods: QuickPickItem[] = []; - let match; - - // Extract all method names that match the regular expression - while ((match = methodRegExp.exec(fileContent)) !== null) { - const methodName = match[3]; - methods.push({ - label: methodName, - description: apexClassPath - }); - } - - // Sort the methods alphabetically by name - methods.sort((a, b) => a.label.localeCompare(b.label)); - - return Promise.resolve(methods); - }; - - public createApexActionFromMethod = async (methodIdentifier: string): Promise => { + public createApexActionFromMethod = async (): Promise => { const telemetryService = await getTelemetryService(); const progressReporter: Progress = { report: value => { @@ -58,32 +33,46 @@ export class ApexActionController { } }; try { - // Step 1: Validate Method - if (!this.isMethodEligible(methodIdentifier)) { + // // Step 0: Validate Method + // if (!this.isMethodEligible(methodIdentifier)) { + // void notificationService.showErrorMessage( + // '`Method ${methodIdentifier} is not eligible for Apex Action creation.`' + // ); + // throw new Error(`Method ${methodIdentifier} is not eligible for Apex Action creation.`); + // } + + // Step 1: Extract Metadata + progressReporter.report({ message: 'Extracting metadata.' }); + const metadata = this.metadataOrchestrator.extractMethodMetadata(); + if (!metadata) { + void notificationService.showErrorMessage('Failed to extract metadata from selected method.'); + throw new Error('Failed to extract metadata from selected method.'); + } + + // Step 2: Validate Method + if (!this.metadataOrchestrator.validateAuraEnabledMethod(metadata.isAuraEnabled)) { void notificationService.showErrorMessage( - '`Method ${methodIdentifier} is not eligible for Apex Action creation.`' + `Method ${metadata.name} is not eligible for Apex Action creation. It is NOT annotated with @AuraEnabled.` + ); + throw new Error( + `Method ${metadata.name} is not eligible for Apex Action creation. It is NOT annotated with @AuraEnabled.` ); - throw new Error(`Method ${methodIdentifier} is not eligible for Apex Action creation.`); } - // Step 2: Extract Metadata - progressReporter.report({ message: 'Extracting metadata.' }); - const metadata = await this.metadataOrchestrator.extractMethodMetadata(methodIdentifier); - // Step 3: Generate OpenAPI Document progressReporter.report({ message: 'Generating OpenAPI document.' }); const openApiDocument = this.generateOpenAPIDocument(metadata); // Step 4: Write OpenAPI Document to File - const openApiFilePath = `${methodIdentifier}_openapi.json`; + const openApiFilePath = `${metadata.name}_openapi.yml`; await this.saveDocument(openApiFilePath, openApiDocument); // Step 6: Notify Success - notificationService.showInformationMessage(`Apex Action created for method: ${methodIdentifier}`); - telemetryService.sendEventData('ApexActionCreated', { method: methodIdentifier }); + notificationService.showInformationMessage(`Apex Action created for method: ${metadata.name}.`); + telemetryService.sendEventData('ApexActionCreated', { method: metadata.name }); } catch (error) { // Error Handling - notificationService.showErrorMessage(`Failed to create Apex Action: ${error.message}`); + notificationService.showErrorMessage(`Failed to create Apex Action: ${error.message}.`); telemetryService.sendException('ApexActionCreationFailed', error); throw error; } @@ -100,26 +89,38 @@ export class ApexActionController { fs.mkdirSync(openAPIdocumentsPath); } const saveLocation = path.join(openAPIdocumentsPath, fileName); - fs.writeFileSync(saveLocation, JSON.stringify(content)); + fs.writeFileSync(saveLocation, content); await vscode.workspace.openTextDocument(saveLocation).then((newDocument: any) => { void vscode.window.showTextDocument(newDocument); }); }; - public generateOpenAPIDocument = (metadata: any): OpenAPIV3.Document => { + public generateOpenAPIDocument = (metadata: MethodMetadata): string => { // Placeholder for OpenAPI generation logic - return { + // ProgressNotification.show(execution, cancellationTokenSource); + const openAPIDocument: OpenAPIV3.Document = { openapi: '3.0.0', info: { title: 'Apex Actions', version: '1.0.0' }, paths: { - [`/apex/${metadata}`]: { + [`/apex/${metadata.name}`]: { post: { - summary: `Invoke ${metadata}`, - operationId: metadata, - responses: { 200: { description: 'Success' } } + operationId: metadata.name, + summary: `Invoke ${metadata.name}`, + parameters: metadata.parameters as unknown as (OpenAPIV3.ReferenceObject | OpenAPIV3.ParameterObject)[], + responses: { + 200: { + description: 'Success', + content: { + 'application/json': { schema: { type: metadata.returnType as OpenAPIV3.NonArraySchemaObjectType } } + } + } + } } } } }; + + // Convert the OpenAPI document to YAML + return stringify(openAPIDocument); }; } diff --git a/packages/salesforcedx-vscode-apex/src/commands/createApexAction.ts b/packages/salesforcedx-vscode-apex/src/commands/createApexAction.ts index 5848886c6f..16989f0428 100644 --- a/packages/salesforcedx-vscode-apex/src/commands/createApexAction.ts +++ b/packages/salesforcedx-vscode-apex/src/commands/createApexAction.ts @@ -4,74 +4,13 @@ * Licensed under the BSD 3-Clause license. * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ -import { notificationService } from '@salesforce/salesforcedx-utils-vscode'; -import { readFileSync } from 'fs'; -import * as vscode from 'vscode'; import { ApexActionController } from './apexActionController'; import { MetadataOrchestrator } from './metadataOrchestrator'; const metadataOrchestrator = new MetadataOrchestrator(); const controller = new ApexActionController(metadataOrchestrator); -const validateAuraEnabledMethod = async ( - filePath: string, - cursorPosition: vscode.Position, - selectedMethod: string -): Promise => { - const lineNumber = cursorPosition.line; - // Read the content of the Apex class file - const fileContent = readFileSync(filePath, 'utf-8'); - const lines = fileContent.split('\n'); - - // Start from the current line and search upward for the method declaration - for (let i = lineNumber; i >= 0; i--) { - const line = lines[i].trim(); - - if (line.includes(selectedMethod)) continue; - // Check if the line contains @AuraEnabled - if (line.includes('@AuraEnabled')) { - return; - } - - // Check if the line contains a method declaration (regex matches methods) - const methodRegex = /\b(public|private|protected|global)\s+(static\s+)?[\w<>\[\]]+\s+\w+\s*\(/; - if (methodRegex.test(line)) { - notificationService.showWarningMessage(`The method "${selectedMethod}" is NOT annotated with @AuraEnabled.`); - throw Error(`The method "${selectedMethod}" is NOT annotated with @AuraEnabled.`); - } - } -}; - -export const createApexActionFromMethod = async (methodIdentifier: any): Promise => { - // Step 1: Prompt User to Select a Method - // const selectedMethod = await controller.listApexMethods(); - const editor = vscode.window.activeTextEditor; - if (!editor) { - notificationService.showErrorMessage('No active editor detected'); - throw Error('No active editor detected'); - } - - const document = editor.document; - let selectedMethod; - const cursorPosition = editor.selection.active; // Get cursor position - const lineText = document.lineAt(cursorPosition.line).text.trim(); // Get the line content - - // Regular expression to match a method declaration and extract its name - const methodRegex = /\b(public|private|protected|global)\s+(static\s+)?[\w<>\[\]]+\s+(\w+)\s*\(/; - - const match = methodRegex.exec(lineText); - if (match) { - selectedMethod = match[3]; // The third capture group is the method name - } - - if (!selectedMethod) { - notificationService.showErrorMessage('No method selected'); - return; - } - - const filePath = methodIdentifier.path; - await validateAuraEnabledMethod(filePath, cursorPosition, selectedMethod); - - // Step 2: Call Controller - await controller.createApexActionFromMethod(selectedMethod); +export const createApexActionFromMethod = async (): Promise => { + // Call Controller + await controller.createApexActionFromMethod(); }; diff --git a/packages/salesforcedx-vscode-apex/src/commands/metadataOrchestrator.ts b/packages/salesforcedx-vscode-apex/src/commands/metadataOrchestrator.ts index 36de850bff..f8b287d30c 100644 --- a/packages/salesforcedx-vscode-apex/src/commands/metadataOrchestrator.ts +++ b/packages/salesforcedx-vscode-apex/src/commands/metadataOrchestrator.ts @@ -4,25 +4,109 @@ * Licensed under the BSD 3-Clause license. * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ +import { notificationService } from '@salesforce/salesforcedx-utils-vscode'; +import * as vscode from 'vscode'; +export interface MethodMetadata { + name: string; + parameters: Parameter[]; + returnType: string; + isAuraEnabled: boolean; +} +export interface Parameter { + name: string; + in: string; + required: boolean; + description: string; + schema: { type: string }; +} export class MetadataOrchestrator { constructor() { // Initialization code here } - public async orchestrate(): Promise { - try { - // Orchestration logic here - } catch (error) { - console.error('Error during orchestration:', error); + public extractMethodMetadata = (): MethodMetadata | undefined => { + const editor = vscode.window.activeTextEditor; + if (!editor) { + notificationService.showErrorMessage('No active editor detected.'); + return; } - } - public async extractMethodMetadata(methodIdentifier: any): Promise { - try { - // Logic to extract method metadata here - return methodIdentifier; - } catch (error) { - console.error('Error extracting method metadata:', error); + + const document = editor.document; + const cursorPosition = editor.selection.active; + const lines = document.getText().split('\n'); + const currentLineIndex = cursorPosition.line; + + let methodSignature = ''; + let isAuraEnabled = false; + + // Check if the preceding line contains @AuraEnabled + if (currentLineIndex > 0 && lines[currentLineIndex - 1].includes('@AuraEnabled')) { + isAuraEnabled = true; } - return undefined; - } + + // Traverse lines starting from the cursor position to construct the method signature + for (let i = currentLineIndex; i < lines.length; i++) { + const line = lines[i].trim(); + methodSignature += ` ${line}`; + + // Stop once the closing parenthesis is reached + if (line.includes(')')) { + break; + } + } + + if (!methodSignature) { + notificationService.showWarningMessage('No valid method found at cursor position.'); + return; + } + + // Parse the method signature + const methodRegex = /\b(public|private|protected|global)\s+(static\s+)?([\w<>\[\]]+)\s+(\w+)\s*\((.*?)\)/s; + const match = methodRegex.exec(methodSignature); + if (!match) { + notificationService.showWarningMessage('Failed to parse method signature.'); + throw Error('Failed to parse method signature.'); + } + const returnType = match[3]; + const methodName = match[4]; + const parametersRaw = match[5] ? match[5].split(',').map(param => param.trim()) : []; + const parameters = parametersRaw.map(param => { + const [type, name] = param.split(/\s+/); + return { + name, + in: 'query', + required: true, + description: `The ${name} parameter of type ${type}.`, + schema: { type: this.mapApexTypeToJsonType(type) } + }; + }); + return { + name: methodName, + parameters, + returnType, + isAuraEnabled + }; + }; + private mapApexTypeToJsonType = (apexType: string): string => { + switch (apexType.toLowerCase()) { + case 'string': + return 'string'; + case 'integer': + case 'int': + case 'long': + return 'integer'; + case 'boolean': + return 'boolean'; + case 'decimal': + case 'double': + case 'float': + return 'number'; + default: + return 'string'; + } + }; + + public validateAuraEnabledMethod = (isAuraEnabled: boolean): boolean => { + return isAuraEnabled; + }; } From 7b2866333513076720a0a692f6979e54f1c32b59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristina=20Isabel=20Ca=C3=B1izales?= Date: Mon, 18 Nov 2024 22:23:29 -0300 Subject: [PATCH 3/7] feat: create apex action from this class --- .../salesforcedx-vscode-apex/package.json | 12 +-- .../package.nls.ja.json | 1 + .../salesforcedx-vscode-apex/package.nls.json | 1 + .../src/commands/apexActionController.ts | 90 ++++++++++++++----- .../src/commands/createApexAction.ts | 5 ++ .../src/commands/index.ts | 2 +- .../src/commands/metadataOrchestrator.ts | 80 +++++++++++++++++ .../salesforcedx-vscode-apex/src/index.ts | 6 ++ 8 files changed, 167 insertions(+), 30 deletions(-) diff --git a/packages/salesforcedx-vscode-apex/package.json b/packages/salesforcedx-vscode-apex/package.json index 8121e514d6..b358a04847 100644 --- a/packages/salesforcedx-vscode-apex/package.json +++ b/packages/salesforcedx-vscode-apex/package.json @@ -130,11 +130,9 @@ { "command": "sf.create.apex.action.method", "when": "sf:project_opened && sf:has_target_org && resource =~ /.\\.(cls)?$/" - } - ], - "explorer/context": [ + }, { - "command": "sf.create.apex.action.method", + "command": "sf.create.apex.action.class", "when": "sf:project_opened && sf:has_target_org && resource =~ /.\\.(cls)?$/" } ], @@ -249,7 +247,7 @@ "when": "sf:project_opened" }, { - "command": "sf.create.apex.action.method", + "command": "sf.create.apex.action.class", "when": "sf:project_opened && sf:has_target_org" } ] @@ -358,6 +356,10 @@ { "command": "sf.create.apex.action.method", "title": "%create_apex_action_method%" + }, + { + "command": "sf.create.apex.action.class", + "title": "%create_apex_action_class%" } ], "configuration": { diff --git a/packages/salesforcedx-vscode-apex/package.nls.ja.json b/packages/salesforcedx-vscode-apex/package.nls.ja.json index caac5f9cb5..1293c8fa50 100644 --- a/packages/salesforcedx-vscode-apex/package.nls.ja.json +++ b/packages/salesforcedx-vscode-apex/package.nls.ja.json @@ -20,6 +20,7 @@ "configuration_title": "Salesforce Apex Configuration", "collapse_tests_title": "SFDX: Apex テストを隠す", "create_apex_action_method": "SFDX: Create Apex Action from Selected Method", + "create_apex_action_class": "SFDX: Create Apex Action from This Class", "go_to_definition_title": "定義に移動", "java_home_description": "Specifies the folder path to the Java 11, Java 17, or Java 21 runtime used to launch the Apex Language Server. Note on Windows the backslashes must be escaped.\n\nMac Example: `/Library/Java/JavaVirtualMachines/openjdk-11.jdk/Contents/Home`\n\nWindows Example: `C:\\\\Program Files\\\\Zulu\\\\zulu-17`\n\nLinux Example: `/usr/lib/jvm/java-21-openjdk-amd64`", "refresh_test_title": "テストを更新", diff --git a/packages/salesforcedx-vscode-apex/package.nls.json b/packages/salesforcedx-vscode-apex/package.nls.json index 1b1f13a139..7e38d3a02c 100644 --- a/packages/salesforcedx-vscode-apex/package.nls.json +++ b/packages/salesforcedx-vscode-apex/package.nls.json @@ -20,6 +20,7 @@ "configuration_title": "Salesforce Apex Configuration", "collapse_tests_title": "SFDX: Collapse All Apex Tests", "create_apex_action_method": "SFDX: Create Apex Action from Selected Method", + "create_apex_action_class": "SFDX: Create Apex Action from This Class", "go_to_definition_title": "Go to Definition", "java_home_description": "Specifies the folder path to the Java 11, Java 17, or Java 21 runtime used to launch the Apex Language Server. Note on Windows the backslashes must be escaped.\n\nMac Example: `/Library/Java/JavaVirtualMachines/openjdk-11.jdk/Contents/Home`\n\nWindows Example: `C:\\\\Program Files\\\\Zulu\\\\zulu-17`\n\nLinux Example: `/usr/lib/jvm/java-21-openjdk-amd64`", "java_memory_description": "Specifies the amount of memory allocation to the Apex Language Server in MB, or null to use the system default value.", diff --git a/packages/salesforcedx-vscode-apex/src/commands/apexActionController.ts b/packages/salesforcedx-vscode-apex/src/commands/apexActionController.ts index 48c56396cc..c6d5790302 100644 --- a/packages/salesforcedx-vscode-apex/src/commands/apexActionController.ts +++ b/packages/salesforcedx-vscode-apex/src/commands/apexActionController.ts @@ -45,15 +45,11 @@ export class ApexActionController { progressReporter.report({ message: 'Extracting metadata.' }); const metadata = this.metadataOrchestrator.extractMethodMetadata(); if (!metadata) { - void notificationService.showErrorMessage('Failed to extract metadata from selected method.'); throw new Error('Failed to extract metadata from selected method.'); } // Step 2: Validate Method if (!this.metadataOrchestrator.validateAuraEnabledMethod(metadata.isAuraEnabled)) { - void notificationService.showErrorMessage( - `Method ${metadata.name} is not eligible for Apex Action creation. It is NOT annotated with @AuraEnabled.` - ); throw new Error( `Method ${metadata.name} is not eligible for Apex Action creation. It is NOT annotated with @AuraEnabled.` ); @@ -61,7 +57,7 @@ export class ApexActionController { // Step 3: Generate OpenAPI Document progressReporter.report({ message: 'Generating OpenAPI document.' }); - const openApiDocument = this.generateOpenAPIDocument(metadata); + const openApiDocument = this.generateOpenAPIDocument([metadata]); // Step 4: Write OpenAPI Document to File const openApiFilePath = `${metadata.name}_openapi.yml`; @@ -78,6 +74,50 @@ export class ApexActionController { } }; + public createApexActionFromClass = async (): Promise => { + const telemetryService = await getTelemetryService(); + const progressReporter: Progress = { + report: value => { + if (value.type === 'StreamingClientProgress' || value.type === 'FormatTestResultProgress') { + this.progress?.report({ message: value.message }); + } + } + }; + try { + // // Step 0: Validate Method + // if (!this.isMethodEligible(methodIdentifier)) { + // void notificationService.showErrorMessage( + // '`Method ${methodIdentifier} is not eligible for Apex Action creation.`' + // ); + // throw new Error(`Method ${methodIdentifier} is not eligible for Apex Action creation.`); + // } + + // Step 1: Extract Metadata + progressReporter.report({ message: 'Extracting metadata.' }); + const metadata = this.metadataOrchestrator.extractAllMethodsMetadata(); + if (!metadata) { + throw new Error('Failed to extract metadata from class.'); + } + + // Step 2: Generate OpenAPI Document + progressReporter.report({ message: 'Generating OpenAPI document.' }); + const openApiDocument = this.generateOpenAPIDocument(metadata); + + // Step 3: Write OpenAPI Document to File + const openApiFilePath = `${metadata[0].name}_openapi.yml`; + await this.saveDocument(openApiFilePath, openApiDocument); + + // Step 4: Notify Success + notificationService.showInformationMessage(`Apex Action created for class: ${metadata[0].name}.`); + telemetryService.sendEventData('ApexActionCreated', { method: metadata[0].name }); + } catch (error) { + // Error Handling + notificationService.showErrorMessage(`Failed to create Apex Action: ${error.message}.`); + telemetryService.sendException('ApexActionCreationFailed', error); + throw error; + } + }; + public isMethodEligible = (methodIdentifier: string): boolean => { // Placeholder for eligibility logic return true; @@ -95,31 +135,33 @@ export class ApexActionController { }); }; - public generateOpenAPIDocument = (metadata: MethodMetadata): string => { + public generateOpenAPIDocument = (metadata: MethodMetadata[]): string => { // Placeholder for OpenAPI generation logic - // ProgressNotification.show(execution, cancellationTokenSource); - const openAPIDocument: OpenAPIV3.Document = { - openapi: '3.0.0', - info: { title: 'Apex Actions', version: '1.0.0' }, - paths: { - [`/apex/${metadata.name}`]: { - post: { - operationId: metadata.name, - summary: `Invoke ${metadata.name}`, - parameters: metadata.parameters as unknown as (OpenAPIV3.ReferenceObject | OpenAPIV3.ParameterObject)[], - responses: { - 200: { - description: 'Success', - content: { - 'application/json': { schema: { type: metadata.returnType as OpenAPIV3.NonArraySchemaObjectType } } - } + const paths: OpenAPIV3.PathsObject = {}; + + metadata.forEach(method => { + paths[`/apex/${method.name}`] = { + post: { + operationId: method.name, + summary: `Invoke ${method.name}`, + parameters: method.parameters as unknown as (OpenAPIV3.ReferenceObject | OpenAPIV3.ParameterObject)[], + responses: { + 200: { + description: 'Success', + content: { + 'application/json': { schema: { type: method.returnType as OpenAPIV3.NonArraySchemaObjectType } } } } } } - } - }; + }; + }); + const openAPIDocument: OpenAPIV3.Document = { + openapi: '3.0.0', + info: { title: 'Apex Actions', version: '1.0.0' }, + paths + }; // Convert the OpenAPI document to YAML return stringify(openAPIDocument); }; diff --git a/packages/salesforcedx-vscode-apex/src/commands/createApexAction.ts b/packages/salesforcedx-vscode-apex/src/commands/createApexAction.ts index 16989f0428..b8a1f8d364 100644 --- a/packages/salesforcedx-vscode-apex/src/commands/createApexAction.ts +++ b/packages/salesforcedx-vscode-apex/src/commands/createApexAction.ts @@ -14,3 +14,8 @@ export const createApexActionFromMethod = async (): Promise => { // Call Controller await controller.createApexActionFromMethod(); }; + +export const createApexActionFromClass = async (): Promise => { + // Call Controller + await controller.createApexActionFromClass(); +}; diff --git a/packages/salesforcedx-vscode-apex/src/commands/index.ts b/packages/salesforcedx-vscode-apex/src/commands/index.ts index 1bd040a42e..41115de4ea 100644 --- a/packages/salesforcedx-vscode-apex/src/commands/index.ts +++ b/packages/salesforcedx-vscode-apex/src/commands/index.ts @@ -18,5 +18,5 @@ export { apexTestMethodRunCodeActionDelegate } from './apexTestRunCodeAction'; export { apexTestSuiteAdd, apexTestSuiteCreate, apexTestSuiteRun } from './apexTestSuite'; -export { createApexActionFromMethod } from './createApexAction'; +export { createApexActionFromMethod, createApexActionFromClass } from './createApexAction'; export { launchApexReplayDebuggerWithCurrentFile } from './launchApexReplayDebuggerWithCurrentFile'; diff --git a/packages/salesforcedx-vscode-apex/src/commands/metadataOrchestrator.ts b/packages/salesforcedx-vscode-apex/src/commands/metadataOrchestrator.ts index f8b287d30c..3983fb1a12 100644 --- a/packages/salesforcedx-vscode-apex/src/commands/metadataOrchestrator.ts +++ b/packages/salesforcedx-vscode-apex/src/commands/metadataOrchestrator.ts @@ -5,12 +5,14 @@ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ import { notificationService } from '@salesforce/salesforcedx-utils-vscode'; +import { stringify } from 'querystring'; import * as vscode from 'vscode'; export interface MethodMetadata { name: string; parameters: Parameter[]; returnType: string; isAuraEnabled: boolean; + className?: string; } export interface Parameter { name: string; @@ -87,6 +89,84 @@ export class MetadataOrchestrator { isAuraEnabled }; }; + + public extractAllMethodsMetadata = (): MethodMetadata[] | undefined => { + const editor = vscode.window.activeTextEditor; + if (!editor) { + notificationService.showErrorMessage('No active editor detected.'); + return; + } + + const document = editor.document; + const filePath = document.fileName; + const className = filePath + .substring(filePath.lastIndexOf(process.platform === 'win32' ? '\\' : '/') + 1) + .split('.') + .shift(); + const lines = document.getText().split('\n'); + const metadataList: MethodMetadata[] = []; + let currentMethodSignature = ''; + let isAuraEnabled = false; + let isEligible = false; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i].trim(); + + // Detect @AuraEnabled annotation + if (line.includes('@AuraEnabled')) { + isAuraEnabled = true; + isEligible = true; + } + + // Build the method signature + currentMethodSignature += ` ${line}`; + if (line.includes(') {') && currentMethodSignature.includes('(')) { + // Method signature is complete + if (isEligible) { + isEligible = false; + const methodRegex = /\b(public|private|protected|global)\s+(static\s+)?([\w<>\[\]]+)\s+(\w+)\s*\((.*?)\)/s; + const match = methodRegex.exec(currentMethodSignature); + if (match) { + const returnType = match[3]; + const methodName = match[4]; + const parametersRaw = match[5] ? match[5].split(',').map(param => param.trim()) : []; + + // Map parameters to the desired structure + const parameters = parametersRaw.map(param => { + const [type, name] = param.split(/\s+/); + return { + name: name || '', + in: 'query', // Assuming query parameters; adjust as needed + required: true, // Assuming all parameters are required; adjust as needed + description: `The ${name || 'parameter'} of type ${type}.`, // Generic description + schema: { type: this.mapApexTypeToJsonType(type || '') } + }; + }); + + metadataList.push({ + name: methodName, + parameters, + returnType, + isAuraEnabled, + className + }); + } + } + + // Reset for the next method + currentMethodSignature = ''; + isAuraEnabled = false; + } + } + + if (metadataList.length === 0) { + notificationService.showWarningMessage('No methods found in the active file.'); + return; + } + + return metadataList; + }; + private mapApexTypeToJsonType = (apexType: string): string => { switch (apexType.toLowerCase()) { case 'string': diff --git a/packages/salesforcedx-vscode-apex/src/index.ts b/packages/salesforcedx-vscode-apex/src/index.ts index ee6f7fd6e7..eb2cdd3d5e 100644 --- a/packages/salesforcedx-vscode-apex/src/index.ts +++ b/packages/salesforcedx-vscode-apex/src/index.ts @@ -27,6 +27,7 @@ import { apexTestSuiteCreate, apexTestSuiteRun, createApexActionFromMethod, + createApexActionFromClass, launchApexReplayDebuggerWithCurrentFile } from './commands'; import { API, SET_JAVA_DOC_LINK } from './constants'; @@ -158,6 +159,10 @@ const registerCommands = (): vscode.Disposable => { 'sf.create.apex.action.method', createApexActionFromMethod ); + const createApexActionFromClassCmd = vscode.commands.registerCommand( + 'sf.create.apex.action.class', + createApexActionFromClass + ); const launchApexReplayDebuggerWithCurrentFileCmd = vscode.commands.registerCommand( 'sf.launch.apex.replay.debugger.with.current.file', launchApexReplayDebuggerWithCurrentFile @@ -184,6 +189,7 @@ const registerCommands = (): vscode.Disposable => { apexTestSuiteRunCmd, apexTestSuiteAddCmd, createApexActionFromMethodCmd, + createApexActionFromClassCmd, launchApexReplayDebuggerWithCurrentFileCmd ); }; From 85003b62865d90919fd80e5b0d6b30d8df2d452a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristina=20Isabel=20Ca=C3=B1izales?= Date: Mon, 18 Nov 2024 23:12:38 -0300 Subject: [PATCH 4/7] chore: messaging and when clause --- .../salesforcedx-vscode-apex/package.json | 4 +++ .../src/commands/apexActionController.ts | 30 +++++++++---------- .../src/commands/metadataOrchestrator.ts | 16 +++++----- 3 files changed, 28 insertions(+), 22 deletions(-) diff --git a/packages/salesforcedx-vscode-apex/package.json b/packages/salesforcedx-vscode-apex/package.json index b358a04847..f0f1297e4c 100644 --- a/packages/salesforcedx-vscode-apex/package.json +++ b/packages/salesforcedx-vscode-apex/package.json @@ -246,6 +246,10 @@ "command": "sf.test.view.collapseAll", "when": "sf:project_opened" }, + { + "command": "sf.create.apex.action.method", + "when": "false" + }, { "command": "sf.create.apex.action.class", "when": "sf:project_opened && sf:has_target_org" diff --git a/packages/salesforcedx-vscode-apex/src/commands/apexActionController.ts b/packages/salesforcedx-vscode-apex/src/commands/apexActionController.ts index c6d5790302..f53ffee926 100644 --- a/packages/salesforcedx-vscode-apex/src/commands/apexActionController.ts +++ b/packages/salesforcedx-vscode-apex/src/commands/apexActionController.ts @@ -51,7 +51,7 @@ export class ApexActionController { // Step 2: Validate Method if (!this.metadataOrchestrator.validateAuraEnabledMethod(metadata.isAuraEnabled)) { throw new Error( - `Method ${metadata.name} is not eligible for Apex Action creation. It is NOT annotated with @AuraEnabled.` + `Method ${metadata.name} is not eligible for Apex Action creation. It is not annotated with @AuraEnabled.` ); } @@ -68,7 +68,7 @@ export class ApexActionController { telemetryService.sendEventData('ApexActionCreated', { method: metadata.name }); } catch (error) { // Error Handling - notificationService.showErrorMessage(`Failed to create Apex Action: ${error.message}.`); + notificationService.showErrorMessage(`Failed to create Apex Action: ${error.message}`); telemetryService.sendException('ApexActionCreationFailed', error); throw error; } @@ -97,22 +97,22 @@ export class ApexActionController { const metadata = this.metadataOrchestrator.extractAllMethodsMetadata(); if (!metadata) { throw new Error('Failed to extract metadata from class.'); + } else if (metadata.length > 0) { + // Step 2: Generate OpenAPI Document + progressReporter.report({ message: 'Generating OpenAPI document.' }); + const openApiDocument = this.generateOpenAPIDocument(metadata); + + // Step 3: Write OpenAPI Document to File + const openApiFilePath = `${metadata[0].name}_openapi.yml`; + await this.saveDocument(openApiFilePath, openApiDocument); + + // Step 4: Notify Success + notificationService.showInformationMessage(`Apex Action created for class: ${metadata[0].name}.`); + telemetryService.sendEventData('ApexActionCreated', { method: metadata[0].name }); } - - // Step 2: Generate OpenAPI Document - progressReporter.report({ message: 'Generating OpenAPI document.' }); - const openApiDocument = this.generateOpenAPIDocument(metadata); - - // Step 3: Write OpenAPI Document to File - const openApiFilePath = `${metadata[0].name}_openapi.yml`; - await this.saveDocument(openApiFilePath, openApiDocument); - - // Step 4: Notify Success - notificationService.showInformationMessage(`Apex Action created for class: ${metadata[0].name}.`); - telemetryService.sendEventData('ApexActionCreated', { method: metadata[0].name }); } catch (error) { // Error Handling - notificationService.showErrorMessage(`Failed to create Apex Action: ${error.message}.`); + notificationService.showErrorMessage(`Failed to create Apex Action: ${error.message}`); telemetryService.sendException('ApexActionCreationFailed', error); throw error; } diff --git a/packages/salesforcedx-vscode-apex/src/commands/metadataOrchestrator.ts b/packages/salesforcedx-vscode-apex/src/commands/metadataOrchestrator.ts index 3983fb1a12..0a5b443951 100644 --- a/packages/salesforcedx-vscode-apex/src/commands/metadataOrchestrator.ts +++ b/packages/salesforcedx-vscode-apex/src/commands/metadataOrchestrator.ts @@ -135,11 +135,11 @@ export class MetadataOrchestrator { const parameters = parametersRaw.map(param => { const [type, name] = param.split(/\s+/); return { - name: name || '', - in: 'query', // Assuming query parameters; adjust as needed - required: true, // Assuming all parameters are required; adjust as needed - description: `The ${name || 'parameter'} of type ${type}.`, // Generic description - schema: { type: this.mapApexTypeToJsonType(type || '') } + name, + in: 'query', + required: true, + description: `The ${name} parameter of type ${type}.`, + schema: { type: this.mapApexTypeToJsonType(type) } }; }); @@ -160,8 +160,10 @@ export class MetadataOrchestrator { } if (metadataList.length === 0) { - notificationService.showWarningMessage('No methods found in the active file.'); - return; + notificationService.showWarningMessage( + 'No eligible methods found in the open editor. Eligible methods are annotated with @AuraEnabled.' + ); + return metadataList; } return metadataList; From b7ca6c20cc2be1673592c747b4cc48ab8538cc1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristina=20Isabel=20Ca=C3=B1izales?= Date: Tue, 19 Nov 2024 21:35:38 -0300 Subject: [PATCH 5/7] chore: refactor, comments and tests --- .../src/commands/apexActionController.ts | 175 +++++++---------- .../src/commands/createApexAction.ts | 10 +- .../src/commands/metadataOrchestrator.ts | 164 ++++++++-------- .../src/messages/i18n.ts | 11 ++ .../commands/metadataOrchestrator.test.ts | 178 ++++++++++++++++++ 5 files changed, 351 insertions(+), 187 deletions(-) create mode 100644 packages/salesforcedx-vscode-apex/test/jest/commands/metadataOrchestrator.test.ts diff --git a/packages/salesforcedx-vscode-apex/src/commands/apexActionController.ts b/packages/salesforcedx-vscode-apex/src/commands/apexActionController.ts index f53ffee926..7f1d7d6b10 100644 --- a/packages/salesforcedx-vscode-apex/src/commands/apexActionController.ts +++ b/packages/salesforcedx-vscode-apex/src/commands/apexActionController.ts @@ -7,139 +7,92 @@ import { Progress } from '@salesforce/apex-node-bundle'; import { notificationService, workspaceUtils } from '@salesforce/salesforcedx-utils-vscode'; import * as fs from 'fs'; -import { OpenAPIV3 } from 'openapi-types'; // Adjust the import path as necessary +import { OpenAPIV3 } from 'openapi-types'; import * as path from 'path'; import * as vscode from 'vscode'; import { stringify } from 'yaml'; +import { nls } from '../messages'; import { getTelemetryService } from '../telemetry/telemetry'; import { MetadataOrchestrator, MethodMetadata } from './metadataOrchestrator'; export class ApexActionController { - constructor( - private metadataOrchestrator: MetadataOrchestrator, // Dependency Injection - private progress?: Progress<{ - message?: string | undefined; - increment?: number | undefined; - }> - ) {} - - public createApexActionFromMethod = async (): Promise => { + constructor(private metadataOrchestrator: MetadataOrchestrator) {} + + /** + * Creates an Apex Action. + * @param isClass - Indicates if the action is for a class or a method. + */ + public createApexAction = async (isClass: boolean): Promise => { + const type = isClass ? 'Class' : 'Method'; + const command = isClass + ? 'SFDX: Create Apex Action from This Class' + : 'SFDX: Create Apex Action from Selected Method'; + let metadata; + let name; const telemetryService = await getTelemetryService(); - const progressReporter: Progress = { - report: value => { - if (value.type === 'StreamingClientProgress' || value.type === 'FormatTestResultProgress') { - this.progress?.report({ message: value.message }); - } - } - }; try { - // // Step 0: Validate Method - // if (!this.isMethodEligible(methodIdentifier)) { - // void notificationService.showErrorMessage( - // '`Method ${methodIdentifier} is not eligible for Apex Action creation.`' - // ); - // throw new Error(`Method ${methodIdentifier} is not eligible for Apex Action creation.`); - // } - - // Step 1: Extract Metadata - progressReporter.report({ message: 'Extracting metadata.' }); - const metadata = this.metadataOrchestrator.extractMethodMetadata(); - if (!metadata) { - throw new Error('Failed to extract metadata from selected method.'); - } - - // Step 2: Validate Method - if (!this.metadataOrchestrator.validateAuraEnabledMethod(metadata.isAuraEnabled)) { - throw new Error( - `Method ${metadata.name} is not eligible for Apex Action creation. It is not annotated with @AuraEnabled.` - ); - } - - // Step 3: Generate OpenAPI Document - progressReporter.report({ message: 'Generating OpenAPI document.' }); - const openApiDocument = this.generateOpenAPIDocument([metadata]); - - // Step 4: Write OpenAPI Document to File - const openApiFilePath = `${metadata.name}_openapi.yml`; - await this.saveDocument(openApiFilePath, openApiDocument); + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: command, + cancellable: true + }, + async progress => { + // Step 1: Extract Metadata + progress.report({ message: nls.localize('extract_metadata') }); + metadata = isClass + ? this.metadataOrchestrator.extractAllMethodsMetadata() + : this.metadataOrchestrator.extractMethodMetadata(); + if (!metadata) { + throw new Error(nls.localize('extraction_failed', type)); + } - // Step 6: Notify Success - notificationService.showInformationMessage(`Apex Action created for method: ${metadata.name}.`); - telemetryService.sendEventData('ApexActionCreated', { method: metadata.name }); - } catch (error) { - // Error Handling - notificationService.showErrorMessage(`Failed to create Apex Action: ${error.message}`); - telemetryService.sendException('ApexActionCreationFailed', error); - throw error; - } - }; + // Step 3: Generate OpenAPI Document + progress.report({ message: nls.localize('generate_openapi_document') }); + const openApiDocument = this.generateOpenAPIDocument(Array.isArray(metadata) ? metadata : [metadata]); - public createApexActionFromClass = async (): Promise => { - const telemetryService = await getTelemetryService(); - const progressReporter: Progress = { - report: value => { - if (value.type === 'StreamingClientProgress' || value.type === 'FormatTestResultProgress') { - this.progress?.report({ message: value.message }); + // Step 4: Write OpenAPI Document to File + name = Array.isArray(metadata) ? metadata[0].className : metadata.name; + const openApiFileName = `${name}_openapi.yml`; + progress.report({ message: nls.localize('write_openapi_document_to_file') }); + await this.saveAndOpenDocument(openApiFileName, openApiDocument); } - } - }; - try { - // // Step 0: Validate Method - // if (!this.isMethodEligible(methodIdentifier)) { - // void notificationService.showErrorMessage( - // '`Method ${methodIdentifier} is not eligible for Apex Action creation.`' - // ); - // throw new Error(`Method ${methodIdentifier} is not eligible for Apex Action creation.`); - // } - - // Step 1: Extract Metadata - progressReporter.report({ message: 'Extracting metadata.' }); - const metadata = this.metadataOrchestrator.extractAllMethodsMetadata(); - if (!metadata) { - throw new Error('Failed to extract metadata from class.'); - } else if (metadata.length > 0) { - // Step 2: Generate OpenAPI Document - progressReporter.report({ message: 'Generating OpenAPI document.' }); - const openApiDocument = this.generateOpenAPIDocument(metadata); - - // Step 3: Write OpenAPI Document to File - const openApiFilePath = `${metadata[0].name}_openapi.yml`; - await this.saveDocument(openApiFilePath, openApiDocument); + ); - // Step 4: Notify Success - notificationService.showInformationMessage(`Apex Action created for class: ${metadata[0].name}.`); - telemetryService.sendEventData('ApexActionCreated', { method: metadata[0].name }); - } - } catch (error) { - // Error Handling - notificationService.showErrorMessage(`Failed to create Apex Action: ${error.message}`); - telemetryService.sendException('ApexActionCreationFailed', error); - throw error; + // Step 5: Notify Success + notificationService.showInformationMessage(nls.localize('apex_action_created', type.toLowerCase(), name)); + telemetryService.sendEventData(`ApexAction${type}Created`, { method: name! }); + } catch (error: any) { + void this.handleError(error, `ApexAction${type}CreationFailed`); } }; - public isMethodEligible = (methodIdentifier: string): boolean => { - // Placeholder for eligibility logic - return true; - }; - - private saveDocument = async (fileName: string, content: any): Promise => { + /** + * Saves and opens the OpenAPI document to a file. + * @param fileName - The name of the file. + * @param content - The content of the file. + */ + private saveAndOpenDocument = async (fileName: string, content: string): Promise => { const openAPIdocumentsPath = path.join(workspaceUtils.getRootWorkspacePath(), 'OpenAPIdocuments'); if (!fs.existsSync(openAPIdocumentsPath)) { fs.mkdirSync(openAPIdocumentsPath); } const saveLocation = path.join(openAPIdocumentsPath, fileName); fs.writeFileSync(saveLocation, content); - await vscode.workspace.openTextDocument(saveLocation).then((newDocument: any) => { + await vscode.workspace.openTextDocument(saveLocation).then((newDocument: vscode.TextDocument) => { void vscode.window.showTextDocument(newDocument); }); }; - public generateOpenAPIDocument = (metadata: MethodMetadata[]): string => { - // Placeholder for OpenAPI generation logic + /** + * Generates an OpenAPI document from the provided metadata. + * @param metadata - The metadata of the methods. + * @returns The OpenAPI document as a string. + */ + private generateOpenAPIDocument = (metadata: MethodMetadata[]): string => { const paths: OpenAPIV3.PathsObject = {}; - metadata.forEach(method => { + metadata?.forEach(method => { paths[`/apex/${method.name}`] = { post: { operationId: method.name, @@ -165,4 +118,16 @@ export class ApexActionController { // Convert the OpenAPI document to YAML return stringify(openAPIDocument); }; + + /** + * Handles errors by showing a notification and sending telemetry data. + * @param error - The error to handle. + * @param telemetryEvent - The telemetry event name. + */ + private handleError = async (error: any, telemetryEvent: string): Promise => { + const telemetryService = await getTelemetryService(); + const errorMessage = error instanceof Error ? error.message : String(error); + notificationService.showErrorMessage(`${nls.localize('create_apex_action_failed')}: ${errorMessage}`); + telemetryService.sendException(telemetryEvent, errorMessage); + }; } diff --git a/packages/salesforcedx-vscode-apex/src/commands/createApexAction.ts b/packages/salesforcedx-vscode-apex/src/commands/createApexAction.ts index b8a1f8d364..f37608f41c 100644 --- a/packages/salesforcedx-vscode-apex/src/commands/createApexAction.ts +++ b/packages/salesforcedx-vscode-apex/src/commands/createApexAction.ts @@ -10,12 +10,18 @@ import { MetadataOrchestrator } from './metadataOrchestrator'; const metadataOrchestrator = new MetadataOrchestrator(); const controller = new ApexActionController(metadataOrchestrator); +/** + * Creates an Apex Action from the method at the current cursor position. + */ export const createApexActionFromMethod = async (): Promise => { // Call Controller - await controller.createApexActionFromMethod(); + await controller.createApexAction(false); }; +/** + * Creates Apex Actions from all methods in the current class. + */ export const createApexActionFromClass = async (): Promise => { // Call Controller - await controller.createApexActionFromClass(); + await controller.createApexAction(true); }; diff --git a/packages/salesforcedx-vscode-apex/src/commands/metadataOrchestrator.ts b/packages/salesforcedx-vscode-apex/src/commands/metadataOrchestrator.ts index 0a5b443951..be266bfeda 100644 --- a/packages/salesforcedx-vscode-apex/src/commands/metadataOrchestrator.ts +++ b/packages/salesforcedx-vscode-apex/src/commands/metadataOrchestrator.ts @@ -5,8 +5,12 @@ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ import { notificationService } from '@salesforce/salesforcedx-utils-vscode'; -import { stringify } from 'querystring'; import * as vscode from 'vscode'; +import { nls } from '../messages'; + +/** + * Interface representing the metadata of a method. + */ export interface MethodMetadata { name: string; parameters: Parameter[]; @@ -14,6 +18,10 @@ export interface MethodMetadata { isAuraEnabled: boolean; className?: string; } + +/** + * Interface representing a parameter of a method. + */ export interface Parameter { name: string; in: string; @@ -21,15 +29,29 @@ export interface Parameter { description: string; schema: { type: string }; } + +/** + * Class responsible for orchestrating metadata operations. + */ export class MetadataOrchestrator { - constructor() { - // Initialization code here - } + /** + * Checks if a method is eligible for Apex Action creation. + * @param methodIdentifier - The identifier of the method. + * @returns True if the method is eligible, false otherwise. + */ + public isMethodEligible = (methodIdentifier: string): boolean => { + // Placeholder for eligibility logic + return true; + }; + /** + * Extracts metadata for the method at the current cursor position. + * @returns The metadata of the method, or undefined if no method is found. + */ public extractMethodMetadata = (): MethodMetadata | undefined => { const editor = vscode.window.activeTextEditor; if (!editor) { - notificationService.showErrorMessage('No active editor detected.'); + notificationService.showErrorMessage(nls.localize('no_active_editor')); return; } @@ -47,53 +69,27 @@ export class MetadataOrchestrator { } // Traverse lines starting from the cursor position to construct the method signature - for (let i = currentLineIndex; i < lines.length; i++) { - const line = lines[i].trim(); + for (let line of lines) { + line = line.trim(); methodSignature += ` ${line}`; // Stop once the closing parenthesis is reached - if (line.includes(')')) { + if (line.includes(') {')) { break; } } - if (!methodSignature) { - notificationService.showWarningMessage('No valid method found at cursor position.'); - return; + const methodMetadata = this.parseMethodSignature(methodSignature, isAuraEnabled); + if (!methodMetadata.isAuraEnabled) { + throw new Error(nls.localize('not_aura_enabled', methodMetadata.name)); } - - // Parse the method signature - const methodRegex = /\b(public|private|protected|global)\s+(static\s+)?([\w<>\[\]]+)\s+(\w+)\s*\((.*?)\)/s; - const match = methodRegex.exec(methodSignature); - if (!match) { - notificationService.showWarningMessage('Failed to parse method signature.'); - throw Error('Failed to parse method signature.'); - } - const returnType = match[3]; - const methodName = match[4]; - const parametersRaw = match[5] ? match[5].split(',').map(param => param.trim()) : []; - const parameters = parametersRaw.map(param => { - const [type, name] = param.split(/\s+/); - return { - name, - in: 'query', - required: true, - description: `The ${name} parameter of type ${type}.`, - schema: { type: this.mapApexTypeToJsonType(type) } - }; - }); - return { - name: methodName, - parameters, - returnType, - isAuraEnabled - }; + return methodMetadata; }; public extractAllMethodsMetadata = (): MethodMetadata[] | undefined => { const editor = vscode.window.activeTextEditor; if (!editor) { - notificationService.showErrorMessage('No active editor detected.'); + notificationService.showErrorMessage(nls.localize('no_active_editor')); return; } @@ -107,68 +103,80 @@ export class MetadataOrchestrator { const metadataList: MethodMetadata[] = []; let currentMethodSignature = ''; let isAuraEnabled = false; - let isEligible = false; - for (let i = 0; i < lines.length; i++) { - const line = lines[i].trim(); + for (let line of lines) { + line = line.trim(); // Detect @AuraEnabled annotation if (line.includes('@AuraEnabled')) { isAuraEnabled = true; - isEligible = true; } // Build the method signature currentMethodSignature += ` ${line}`; if (line.includes(') {') && currentMethodSignature.includes('(')) { // Method signature is complete - if (isEligible) { - isEligible = false; - const methodRegex = /\b(public|private|protected|global)\s+(static\s+)?([\w<>\[\]]+)\s+(\w+)\s*\((.*?)\)/s; - const match = methodRegex.exec(currentMethodSignature); - if (match) { - const returnType = match[3]; - const methodName = match[4]; - const parametersRaw = match[5] ? match[5].split(',').map(param => param.trim()) : []; - - // Map parameters to the desired structure - const parameters = parametersRaw.map(param => { - const [type, name] = param.split(/\s+/); - return { - name, - in: 'query', - required: true, - description: `The ${name} parameter of type ${type}.`, - schema: { type: this.mapApexTypeToJsonType(type) } - }; - }); - - metadataList.push({ - name: methodName, - parameters, - returnType, - isAuraEnabled, - className - }); + if (isAuraEnabled) { + const methodMetadata = this.parseMethodSignature(currentMethodSignature, isAuraEnabled, className); + if (methodMetadata) { + metadataList.push(methodMetadata); } + isAuraEnabled = false; } // Reset for the next method currentMethodSignature = ''; - isAuraEnabled = false; } } if (metadataList.length === 0) { - notificationService.showWarningMessage( - 'No eligible methods found in the open editor. Eligible methods are annotated with @AuraEnabled.' - ); - return metadataList; + throw new Error(nls.localize('no_eligible_methods_found')); } return metadataList; }; + /** + * Parses a method signature and returns the method metadata. + * @param methodSignature - The method signature to parse. + * @param isAuraEnabled - Indicates if the method is Aura-enabled. + * @param className - The name of the class containing the method. + * @returns The metadata of the method, or undefined if parsing fails. + */ + private parseMethodSignature(methodSignature: string, isAuraEnabled: boolean, className?: string): MethodMetadata { + const methodRegex = /\b(public|private|protected|global)\s+(static\s+)?([\w<>\[\]]+)\s+(\w+)\s*\((.*?)\)/s; + const match = methodRegex.exec(methodSignature); + if (!match) { + throw Error(nls.localize('no_valid_method_found')); + } + const returnType = match[3]; + const methodName = match[4]; + const parametersRaw = match[5] ? match[5].split(',').map(param => param.trim()) : []; + const parameters = parametersRaw.map(param => { + const [type, name] = param.split(/\s+/); + return { + name, + in: 'query', + required: true, + description: `The ${name} parameter of type ${type}.`, + schema: { type: this.mapApexTypeToJsonType(type) } + }; + }); + + return { + name: methodName, + parameters, + returnType, + isAuraEnabled, + className + }; + } + + /** + * Maps an Apex type to a JSON type. + * @param apexType - The Apex type to map. + * @returns The corresponding JSON type. + */ private mapApexTypeToJsonType = (apexType: string): string => { switch (apexType.toLowerCase()) { case 'string': @@ -187,8 +195,4 @@ export class MetadataOrchestrator { return 'string'; } }; - - public validateAuraEnabledMethod = (isAuraEnabled: boolean): boolean => { - return isAuraEnabled; - }; } diff --git a/packages/salesforcedx-vscode-apex/src/messages/i18n.ts b/packages/salesforcedx-vscode-apex/src/messages/i18n.ts index 768543dda1..90e7b5ccc2 100644 --- a/packages/salesforcedx-vscode-apex/src/messages/i18n.ts +++ b/packages/salesforcedx-vscode-apex/src/messages/i18n.ts @@ -52,6 +52,17 @@ export const messages = { 'Test class not provided. Run the code action on a class annotated with @isTest.', apex_test_run_codeAction_no_method_test_param_text: 'Test method not provided. Run the code action on a method annotated with @isTest or testMethod.', + create_apex_action_failed: 'Failed to create Apex Action', + extract_metadata: 'Extracting metadata.', + extraction_failed: 'Failed to extract metadata from %s', + apex_action_created: 'Apex Action created for %s: %s.', + generate_openapi_document: 'Generating OpenAPI document.', + write_openapi_document_to_file: 'Writing OpenAPI document to file.', + no_active_editor: 'No active editor detected.', + not_aura_enabled: 'Method %s is not eligible for Apex Action creation. It is not annotated with @AuraEnabled.', + no_valid_method_found: 'No valid method found at cursor position.', + no_eligible_methods_found: + 'No eligible methods found in the open editor. Eligible methods are annotated with @AuraEnabled.', apex_test_run_text: 'SFDX: Run Apex Tests', test_view_loading_message: 'Loading Apex tests ...', test_view_no_tests_message: 'No Apex Tests Found', diff --git a/packages/salesforcedx-vscode-apex/test/jest/commands/metadataOrchestrator.test.ts b/packages/salesforcedx-vscode-apex/test/jest/commands/metadataOrchestrator.test.ts new file mode 100644 index 0000000000..a3e879d37e --- /dev/null +++ b/packages/salesforcedx-vscode-apex/test/jest/commands/metadataOrchestrator.test.ts @@ -0,0 +1,178 @@ +/* + * Copyright (c) 2024, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ +import { notificationService } from '@salesforce/salesforcedx-utils-vscode'; +import * as vscode from 'vscode'; +import { MetadataOrchestrator } from '../../../src/commands/metadataOrchestrator'; + +describe('MetadataOrchestrator', () => { + let orchestrator: MetadataOrchestrator; + let showErrorMessageMock: jest.SpyInstance; + + beforeEach(() => { + orchestrator = new MetadataOrchestrator(); + showErrorMessageMock = jest.spyOn(notificationService, 'showErrorMessage').mockImplementation(jest.fn()); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe('isMethodEligible', () => { + it('should return true for any method identifier', () => { + const result = orchestrator.isMethodEligible('someMethod'); + expect(result).toBe(true); + }); + }); + + describe('extractMethodMetadata', () => { + it('should return undefined if no active editor', () => { + (vscode.window as any).activeTextEditor = undefined; + const result = orchestrator.extractMethodMetadata(); + expect(result).toBeUndefined(); + expect(showErrorMessageMock).toHaveBeenCalledTimes(1); + }); + + it('should return method metadata if method is found', () => { + const editorStub = { + document: { + getText: () => '@AuraEnabled\npublic void someMethod(String param) { }' + }, + selection: { + active: { line: 1 } + } + } as vscode.TextEditor; + (vscode.window as any).activeTextEditor = editorStub; + + const result = orchestrator.extractMethodMetadata(); + expect(result).toEqual({ + name: 'someMethod', + parameters: [ + { + name: 'param', + in: 'query', + required: true, + description: 'The param parameter of type String.', + schema: { type: 'string' } + } + ], + returnType: 'void', + isAuraEnabled: true + }); + }); + + it('should throw an error if method is not Aura-enabled', () => { + const editorStub = { + document: { + uri: { path: 'someClass.cls' } as vscode.Uri, + getText: () => 'public void someMethod(String param) { }', + fileName: 'someClass.cls' + }, + selection: { + active: { line: 0 } + } + } as vscode.TextEditor; + + (vscode.window as any).activeTextEditor = editorStub; + + expect(() => orchestrator.extractMethodMetadata()).toThrow(); + }); + }); + + describe('extractAllMethodsMetadata', () => { + it('should return undefined if no active editor', () => { + (vscode.window as any).activeTextEditor = undefined; + const result = orchestrator.extractAllMethodsMetadata(); + expect(result).toBeUndefined(); + expect(showErrorMessageMock).toHaveBeenCalledTimes(1); + }); + + it('should return metadata for all methods', () => { + const editorStub = { + document: { + uri: { path: 'someClass.cls' } as vscode.Uri, + getText: () => '@AuraEnabled\npublic void methodOne() { }\n@AuraEnabled\npublic void methodTwo() { }', + fileName: 'someClass.cls' + }, + selection: { + active: { line: 1 } + } + } as vscode.TextEditor; + (vscode.window as any).activeTextEditor = editorStub; + + const result = orchestrator.extractAllMethodsMetadata(); + expect(result).toEqual([ + { + name: 'methodOne', + parameters: [], + returnType: 'void', + isAuraEnabled: true, + className: 'someClass' + }, + { + name: 'methodTwo', + parameters: [], + returnType: 'void', + isAuraEnabled: true, + className: 'someClass' + } + ]); + }); + + it('should throw an error if no eligible methods are found', () => { + const editorStub = { + document: { + uri: { path: 'someClass.cls' } as vscode.Uri, + getText: () => 'public void methodOne() { }', + fileName: 'someClass.cls' + }, + selection: { + active: { line: 1 } + } + } as vscode.TextEditor; + (vscode.window as any).activeTextEditor = editorStub; + + expect(() => orchestrator.extractAllMethodsMetadata()).toThrow(); + }); + }); + + describe('parseMethodSignature', () => { + it('should parse method signature and return metadata', () => { + const methodSignature = 'public void someMethod(String param) { }'; + const result = orchestrator['parseMethodSignature'](methodSignature, true, 'someClass'); + expect(result).toEqual({ + name: 'someMethod', + parameters: [ + { + name: 'param', + in: 'query', + required: true, + description: 'The param parameter of type String.', + schema: { type: 'string' } + } + ], + returnType: 'void', + isAuraEnabled: true, + className: 'someClass' + }); + }); + + it('should throw an error if method signature is invalid', () => { + const methodSignature = 'invalid signature'; + expect(() => orchestrator['parseMethodSignature'](methodSignature, true)).toThrow(); + }); + }); + + describe('mapApexTypeToJsonType', () => { + it('should map Apex types to JSON types', () => { + expect(orchestrator['mapApexTypeToJsonType']('String')).toEqual('string'); + expect(orchestrator['mapApexTypeToJsonType']('Integer')).toEqual('integer'); + expect(orchestrator['mapApexTypeToJsonType']('Boolean')).toEqual('boolean'); + expect(orchestrator['mapApexTypeToJsonType']('Double')).toEqual('number'); + expect(orchestrator['mapApexTypeToJsonType']('UnknownType')).toEqual('string'); + }); + }); +}); From 68f5daf67c031bad6a8239e6238056a0e1b0779a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristina=20Isabel=20Ca=C3=B1izales?= Date: Tue, 19 Nov 2024 21:44:46 -0300 Subject: [PATCH 6/7] chore: revert changes --- .../check-feature-request/lib/src/index.js | 152 ++--- .github/actions/new-issue/lib/src/index.js | 109 ++- .../actions/validate-issue/lib/src/index.js | 642 +++++++++--------- .../validate-issue/lib/src/nodeVersions.js | 21 +- 4 files changed, 451 insertions(+), 473 deletions(-) diff --git a/.github/actions/check-feature-request/lib/src/index.js b/.github/actions/check-feature-request/lib/src/index.js index 7d1d5db286..fb1524d324 100644 --- a/.github/actions/check-feature-request/lib/src/index.js +++ b/.github/actions/check-feature-request/lib/src/index.js @@ -1,91 +1,83 @@ -"use strict"; +'use strict'; /* * Copyright (c) 2024, salesforce.com, inc. * All rights reserved. * Licensed under the BSD 3-Clause license. * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ -Object.defineProperty(exports, "__esModule", { value: true }); -const core_1 = require("@actions/core"); -const github_1 = require("@actions/github"); +Object.defineProperty(exports, '__esModule', { value: true }); +const core_1 = require('@actions/core'); +const github_1 = require('@actions/github'); async function run() { - try { - const issue = github_1.context.payload.issue; - if (!issue) { - (0, core_1.setFailed)("github.context.payload.issue does not exist"); - return; - } - // Temporary check to prevent this action from running on old issues - // This will prevent noise on tickets already being investigated - // This can be removed once the action has been running for a while - const creationDate = new Date(issue.created_at); - const cutoffDate = new Date("2023-06-14T00:00:00Z"); - if (creationDate < cutoffDate) { - console.log("Issue was created before 6/14/2023, skipping"); - return; - } - // Create a GitHub client - const token = (0, core_1.getInput)("repo-token"); - const octokit = (0, github_1.getOctokit)(token); - // Get owner and repo from context - const owner = github_1.context.repo.owner; - const repo = github_1.context.repo.repo; - const issue_number = issue.number; - console.log("Issue URL:", issue.html_url); - const { body } = issue; - const { login: author } = issue.user; - const { data: comments } = await getAllComments(); - // For version checks, we only care about comments from the author - const authorComments = comments.filter((comment) => comment?.user?.login === author); - // Build an array of the issue body and all of the comment bodies - const bodies = [ - body, - ...authorComments.map((comment) => comment.body), - ].filter((body) => body !== undefined); - console.log('bodies = ' + JSON.stringify(bodies)); - console.log('bodies.length = ' + bodies.length); - const core = require('@actions/core'); - if (bodies[0] === null) { - core.setOutput("is_feature_request", "false"); - } - else { - const featureRequestRegex = /(feature\s*request)/ig; - // Search all bodies and get an array of all versions found (first capture group) - const featureRequests = bodies - .map((body) => [...body.matchAll(featureRequestRegex)].map((match) => match[1])) - .flat(); - if (featureRequests.length > 0) { - console.log('This issue is a feature request!'); - addLabel("type:enhancements"); - core.setOutput("is_feature_request", "true"); - } - else { - core.setOutput("is_feature_request", "false"); - } - } - // --------- - // FUNCTIONS - // --------- - async function getAllComments() { - return await octokit.rest.issues.listComments({ - owner, - repo, - issue_number, - }); - } - async function addLabel(label) { - await octokit.rest.issues.addLabels({ - owner, - repo, - issue_number, - labels: [label], - }); - } + try { + const issue = github_1.context.payload.issue; + if (!issue) { + (0, core_1.setFailed)('github.context.payload.issue does not exist'); + return; } - catch (err) { - const error = err; - (0, core_1.setFailed)(error.message); + // Temporary check to prevent this action from running on old issues + // This will prevent noise on tickets already being investigated + // This can be removed once the action has been running for a while + const creationDate = new Date(issue.created_at); + const cutoffDate = new Date('2023-06-14T00:00:00Z'); + if (creationDate < cutoffDate) { + console.log('Issue was created before 6/14/2023, skipping'); + return; } + // Create a GitHub client + const token = (0, core_1.getInput)('repo-token'); + const octokit = (0, github_1.getOctokit)(token); + // Get owner and repo from context + const owner = github_1.context.repo.owner; + const repo = github_1.context.repo.repo; + const issue_number = issue.number; + console.log('Issue URL:', issue.html_url); + const { body } = issue; + const { login: author } = issue.user; + const { data: comments } = await getAllComments(); + // For version checks, we only care about comments from the author + const authorComments = comments.filter(comment => comment?.user?.login === author); + // Build an array of the issue body and all of the comment bodies + const bodies = [body, ...authorComments.map(comment => comment.body)].filter(body => body !== undefined); + console.log('bodies = ' + JSON.stringify(bodies)); + console.log('bodies.length = ' + bodies.length); + const core = require('@actions/core'); + if (bodies[0] === null) { + core.setOutput('is_feature_request', 'false'); + } else { + const featureRequestRegex = /(feature\s*request)/gi; + // Search all bodies and get an array of all versions found (first capture group) + const featureRequests = bodies.map(body => [...body.matchAll(featureRequestRegex)].map(match => match[1])).flat(); + if (featureRequests.length > 0) { + console.log('This issue is a feature request!'); + addLabel('type:enhancements'); + core.setOutput('is_feature_request', 'true'); + } else { + core.setOutput('is_feature_request', 'false'); + } + } + // --------- + // FUNCTIONS + // --------- + async function getAllComments() { + return await octokit.rest.issues.listComments({ + owner, + repo, + issue_number + }); + } + async function addLabel(label) { + await octokit.rest.issues.addLabels({ + owner, + repo, + issue_number, + labels: [label] + }); + } + } catch (err) { + const error = err; + (0, core_1.setFailed)(error.message); + } } run(); -//# sourceMappingURL=index.js.map \ No newline at end of file +//# sourceMappingURL=index.js.map diff --git a/.github/actions/new-issue/lib/src/index.js b/.github/actions/new-issue/lib/src/index.js index 6e2c55bfb0..05cc998a32 100644 --- a/.github/actions/new-issue/lib/src/index.js +++ b/.github/actions/new-issue/lib/src/index.js @@ -1,66 +1,65 @@ -"use strict"; +'use strict'; /* * Copyright (c) 2024, salesforce.com, inc. * All rights reserved. * Licensed under the BSD 3-Clause license. * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ -Object.defineProperty(exports, "__esModule", { value: true }); -const core_1 = require("@actions/core"); -const github_1 = require("@actions/github"); +Object.defineProperty(exports, '__esModule', { value: true }); +const core_1 = require('@actions/core'); +const github_1 = require('@actions/github'); async function run() { - try { - // The issue request exists on payload when an issue is created - // Sets action status to failed when issue does not exist on payload. - const issue = github_1.context.payload.issue; - if (!issue) { - (0, core_1.setFailed)("github.context.payload.issue does not exist"); - return; - } - // Get input parameters. - const token = (0, core_1.getInput)("repo-token"); - const message = (0, core_1.getInput)("message"); - const label = (0, core_1.getInput)("label"); - console.log("message: ", message); - console.log("label: ", label); - // Create a GitHub client. - const octokit = (0, github_1.getOctokit)(token); - // Get owner and repo from context - const owner = github_1.context.repo.owner; - const repo = github_1.context.repo.repo; - // Create a comment on Issue - // https://octokit.github.io/rest.js/#octokit-routes-issues-create-comment - console.log("owner: " + owner); - console.log("repo: " + repo); - console.log("issue number: " + issue.number); - const issueLabels = issue.labels; - console.log("issue labels: ", issueLabels); - const { data: comments } = await octokit.rest.issues.listComments({ - owner, - repo, - issue_number: issue.number, - }); - // If we have comments check out that this comment has not been previously commented - if (comments.length) { - if (comments.some((comment) => comment.body === message)) { - console.log("Already commented"); - return; - } - } - const response = await octokit.rest.issues.createComment({ - owner, - repo, - // eslint-disable-next-line @typescript-eslint/camelcase - issue_number: issue.number, - body: message, - }); - console.log("created comment URL: " + response.data.html_url); - (0, core_1.setOutput)("comment-url", response.data.html_url); + try { + // The issue request exists on payload when an issue is created + // Sets action status to failed when issue does not exist on payload. + const issue = github_1.context.payload.issue; + if (!issue) { + (0, core_1.setFailed)('github.context.payload.issue does not exist'); + return; } - catch (err) { - const error = err; - (0, core_1.setFailed)(error.message); + // Get input parameters. + const token = (0, core_1.getInput)('repo-token'); + const message = (0, core_1.getInput)('message'); + const label = (0, core_1.getInput)('label'); + console.log('message: ', message); + console.log('label: ', label); + // Create a GitHub client. + const octokit = (0, github_1.getOctokit)(token); + // Get owner and repo from context + const owner = github_1.context.repo.owner; + const repo = github_1.context.repo.repo; + // Create a comment on Issue + // https://octokit.github.io/rest.js/#octokit-routes-issues-create-comment + console.log('owner: ' + owner); + console.log('repo: ' + repo); + console.log('issue number: ' + issue.number); + const issueLabels = issue.labels; + console.log('issue labels: ', issueLabels); + const { data: comments } = await octokit.rest.issues.listComments({ + owner, + repo, + issue_number: issue.number + }); + // If we have comments check out that this comment has not been previously commented + if (comments.length) { + if (comments.some(comment => comment.body === message)) { + console.log('Already commented'); + return; + } } + const response = await octokit.rest.issues.createComment({ + owner, + repo, + // eslint-disable-next-line @typescript-eslint/camelcase + issue_number: issue.number, + body: message + }); + console.log('created comment URL: ' + response.data.html_url); + (0, core_1.setOutput)('comment-url', response.data.html_url); + } catch (err) { + const error = err; + (0, core_1.setFailed)(error.message); + } } run(); -//# sourceMappingURL=index.js.map \ No newline at end of file +//# sourceMappingURL=index.js.map diff --git a/.github/actions/validate-issue/lib/src/index.js b/.github/actions/validate-issue/lib/src/index.js index 61f9c73f67..5a530661fe 100644 --- a/.github/actions/validate-issue/lib/src/index.js +++ b/.github/actions/validate-issue/lib/src/index.js @@ -1,354 +1,338 @@ -"use strict"; +'use strict'; /* * Copyright (c) 2024, salesforce.com, inc. * All rights reserved. * Licensed under the BSD 3-Clause license. * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ -Object.defineProperty(exports, "__esModule", { value: true }); -const core_1 = require("@actions/core"); -const github_1 = require("@actions/github"); -const child_process_1 = require("child_process"); -const semver = require("semver"); -const fs_1 = require("fs"); -const path = require("path"); -const nodeVersions_1 = require("./nodeVersions"); +Object.defineProperty(exports, '__esModule', { value: true }); +const core_1 = require('@actions/core'); +const github_1 = require('@actions/github'); +const child_process_1 = require('child_process'); +const semver = require('semver'); +const fs_1 = require('fs'); +const path = require('path'); +const nodeVersions_1 = require('./nodeVersions'); async function run() { - try { - const issue = github_1.context.payload.issue; - if (!issue) { - (0, core_1.setFailed)("github.context.payload.issue does not exist"); - return; + try { + const issue = github_1.context.payload.issue; + if (!issue) { + (0, core_1.setFailed)('github.context.payload.issue does not exist'); + return; + } + // Temporary check to prevent this action from running on old issues + // This will prevent noise on tickets already being investigated + // This can be removed once the action has been running for a while + const creationDate = new Date(issue.created_at); + const cutoffDate = new Date('2023-06-14T00:00:00Z'); + if (creationDate < cutoffDate) { + console.log('Issue was created before 6/14/2023, skipping'); + return; + } + // Create a GitHub client + const token = (0, core_1.getInput)('repo-token'); + const octokit = (0, github_1.getOctokit)(token); + // Get owner and repo from context + const owner = github_1.context.repo.owner; + const repo = github_1.context.repo.repo; + const issue_number = issue.number; + console.log('Issue URL:', issue.html_url); + const { body } = issue; + const { login: author } = issue.user; + const { data: comments } = await getAllComments(); + // For version checks, we only care about comments from the author + const authorComments = comments.filter(comment => comment?.user?.login === author); + // Build an array of the issue body and all of the comment bodies + const bodies = [body, ...authorComments.map(comment => comment.body)].filter(body => body !== undefined); + console.log('bodies = ' + JSON.stringify(bodies)); + console.log('bodies.length = ' + bodies.length); + if (bodies[0] === null) { + console.log('No content provided in issue body'); + const message = getFile('../../messages/provide-version.md', { + THE_AUTHOR: issue.user.login + }); + postComment(message); + addLabel('missing required information'); + } else { + let extensionsValid = true; + let vscodeValid = true; + let osVersionValid = true; + let cliValid = true; + let lastWorkingVersionValid = true; + let provideVersionAlreadyRequested = false; + // Checking Salesforce Extension Pack version + // The text "Salesforce Extension Version in VS Code" can be either bolded or unbolded + const extensionsVersionRegex = + /(?:\*{2}Salesforce Extension Version in VS Code\*{2}:\s*v?(\d{2}\.\d{1,2}\.\d))|(?:Salesforce Extension Version in VS Code:\s*v?(\d{2}\.\d{1,2}\.\d))/g; + // Search all bodies and get an array of all versions found (first or second capture group) + const extensionsVersions = bodies + .map(body => [...body.matchAll(extensionsVersionRegex)].map(match => match[1] || match[2])) + .flat(); + console.log('extensionsVersions', extensionsVersions); + if (extensionsVersions.length > 0) { + const extensionsLatest = getLatestExtensionsVersion(); + console.log('extensionsLatest', extensionsLatest); + const oneSatisfies = extensionsVersions.some(version => semver.gte(version, extensionsLatest)); + if (!oneSatisfies) { + const oldExtensions = getFile('../../messages/old-extensions.md', { + THE_AUTHOR: author, + USER_VERSION: extensionsVersions.join('`, `'), + LATEST_VERSION: extensionsLatest + }); + postComment(oldExtensions); } - // Temporary check to prevent this action from running on old issues - // This will prevent noise on tickets already being investigated - // This can be removed once the action has been running for a while - const creationDate = new Date(issue.created_at); - const cutoffDate = new Date("2023-06-14T00:00:00Z"); - if (creationDate < cutoffDate) { - console.log("Issue was created before 6/14/2023, skipping"); - return; + if (extensionsValid) { + console.log('A valid extensions version is provided!'); + } else { + console.log('The extensions version provided is NOT valid'); + addLabel('missing required information'); } - // Create a GitHub client - const token = (0, core_1.getInput)("repo-token"); - const octokit = (0, github_1.getOctokit)(token); - // Get owner and repo from context - const owner = github_1.context.repo.owner; - const repo = github_1.context.repo.repo; - const issue_number = issue.number; - console.log("Issue URL:", issue.html_url); - const { body } = issue; - const { login: author } = issue.user; - const { data: comments } = await getAllComments(); - // For version checks, we only care about comments from the author - const authorComments = comments.filter((comment) => comment?.user?.login === author); - // Build an array of the issue body and all of the comment bodies - const bodies = [ - body, - ...authorComments.map((comment) => comment.body), - ].filter((body) => body !== undefined); - console.log('bodies = ' + JSON.stringify(bodies)); - console.log('bodies.length = ' + bodies.length); - if (bodies[0] === null) { - console.log('No content provided in issue body'); - const message = getFile("../../messages/provide-version.md", { - THE_AUTHOR: issue.user.login, - }); - postComment(message); - addLabel("missing required information"); + } else { + console.log('Extensions version is NOT provided'); + if (!provideVersionAlreadyRequested) { + const message = getFile('../../messages/provide-version.md', { + THE_AUTHOR: issue.user.login + }); + postComment(message); + provideVersionAlreadyRequested = true; + addLabel('missing required information'); } - else { - let extensionsValid = true; - let vscodeValid = true; - let osVersionValid = true; - let cliValid = true; - let lastWorkingVersionValid = true; - let provideVersionAlreadyRequested = false; - // Checking Salesforce Extension Pack version - // The text "Salesforce Extension Version in VS Code" can be either bolded or unbolded - const extensionsVersionRegex = /(?:\*{2}Salesforce Extension Version in VS Code\*{2}:\s*v?(\d{2}\.\d{1,2}\.\d))|(?:Salesforce Extension Version in VS Code:\s*v?(\d{2}\.\d{1,2}\.\d))/g; - // Search all bodies and get an array of all versions found (first or second capture group) - const extensionsVersions = bodies - .map((body) => [...body.matchAll(extensionsVersionRegex)].map((match) => match[1] || match[2])) - .flat(); - console.log('extensionsVersions', extensionsVersions); - if (extensionsVersions.length > 0) { - const extensionsLatest = getLatestExtensionsVersion(); - console.log('extensionsLatest', extensionsLatest); - const oneSatisfies = extensionsVersions.some((version) => semver.gte(version, extensionsLatest)); - if (!oneSatisfies) { - const oldExtensions = getFile("../../messages/old-extensions.md", { - THE_AUTHOR: author, - USER_VERSION: extensionsVersions.join("`, `"), - LATEST_VERSION: extensionsLatest - }); - postComment(oldExtensions); - } - if (extensionsValid) { - console.log("A valid extensions version is provided!"); - } - else { - console.log("The extensions version provided is NOT valid"); - addLabel("missing required information"); - } - } - else { - console.log("Extensions version is NOT provided"); - if (!provideVersionAlreadyRequested) { - const message = getFile("../../messages/provide-version.md", { - THE_AUTHOR: issue.user.login, - }); - postComment(message); - provideVersionAlreadyRequested = true; - addLabel("missing required information"); - } - extensionsValid = false; - } - // Checking VSCode version - const vscodeVersionRegex = /(?:\*{2}VS Code version\*{2}:\s*(?:Version:\s*)?v?(1\.\d{2}\.\d))|(?:VS Code version:\s*(?:Version:\s*)?v?(1\.\d{2}\.\d))/g; - // Search all bodies and get an array of all versions found (first or second capture group) - const vscodeVersions = bodies - .map((body) => [...body.matchAll(vscodeVersionRegex)].map((match) => match[1] || match[2])) - .flat(); - console.log('vscodeVersions', vscodeVersions); - if (vscodeVersions.length > 0) { - const vscodeMinVersion = getMinimumVSCodeVersion(); - console.log('vscodeMinVersion', vscodeMinVersion); - const oneSatisfies = vscodeVersions.some((version) => semver.gte(version, vscodeMinVersion)); - if (!oneSatisfies) { - const oldVSCode = getFile("../../messages/unsupported-vscode.md", { - THE_AUTHOR: author, - USER_VERSION: vscodeVersions.join("`, `"), - MIN_VERSION: vscodeMinVersion - }); - postComment(oldVSCode); - vscodeValid = false; - } - if (vscodeValid) { - console.log("A valid VSCode version is provided!"); - } - else { - console.log("The VSCode version provided is NOT valid"); - addLabel("missing required information"); - } - } - else { - console.log("VSCode version is NOT provided"); - if (!provideVersionAlreadyRequested) { - const message = getFile("../../messages/provide-version.md", { - THE_AUTHOR: issue.user.login, - }); - postComment(message); - provideVersionAlreadyRequested = true; - addLabel("missing required information"); - } - vscodeValid = false; - } - // Checking presence of OS and version - // NOTE: negative lookahead used in this regex due to false match when OS and version is blank - const osVersionRegex = /(?:\*{2}OS and version\*{2}:\s*(?!\*\*VS|VS)\S.*?)(?=\r?\n|$)|(?:OS and version:\s*(?!\*\*VS|VS)\S.*?)(?=\r?\n|$)/g; - // Search all bodies and get an array of all versions found (first or second capture group) - const osVersions = bodies - .map((body) => [...body.matchAll(osVersionRegex)].map((match) => match[1] || match[2])) - .flat(); - if (osVersions.length > 0) { - console.log("OS and version is provided!"); - } - else { - console.log("OS and version is NOT provided"); - if (!provideVersionAlreadyRequested) { - const message = getFile("../../messages/provide-version.md", { - THE_AUTHOR: issue.user.login, - }); - postComment(message); - provideVersionAlreadyRequested = true; - addLabel("missing required information"); - } - osVersionValid = false; - } - // Checking presence of last working extensions version - const lastWorkingVersionRegex = /(\*{2}Most recent version of the extensions where this was working\*{2}:\s*\S.*\r\n)|(Most recent version of the extensions where this was working:\s*\S.*\r\n)|(\*{2}Most recent version of the extensions where this was working\*{2}:\s*\S.*$)|(Most recent version of the extensions where this was working:\s*\S.*$)/g; - // Search all bodies and get an array of all versions found (first or second capture group) - const lastWorkingVersions = bodies - .map((body) => [...body.matchAll(lastWorkingVersionRegex)].map((match) => match[1] || match[2])) - .flat(); - if (lastWorkingVersions.length > 0) { - console.log("Last working version is provided!"); - } - else { - console.log("Last working version is NOT provided"); - if (!provideVersionAlreadyRequested) { - const message = getFile("../../messages/provide-version.md", { - THE_AUTHOR: issue.user.login, - }); - postComment(message); - provideVersionAlreadyRequested = true; - addLabel("missing required information"); - } - lastWorkingVersionValid = false; - } - // *** The below is the check for CLI version, code reused from CLI Team's repo *** - const sfVersionRegex = /(?:Salesforce CLI Version|(?:\*{2}Salesforce CLI Version\*{2})):\s*(?:@salesforce\/cli\/)?(\d+\.\d+\.\d+)/g; - const sfdxVersionRegex = /(?:Salesforce CLI Version|(?:\*{2}Salesforce CLI Version\*{2})):\s*(?:sfdx-cli\/)?(\d+\.\d+\.\d+)/g; - const nodeVersionRegex = /node-v(\d{2})\.\d+\.\d+/g; - // Search all bodies and get an array of all versions found (first capture group) - const sfVersions = bodies - .map((body) => [...body.matchAll(sfVersionRegex)].map((match) => match[1])) - .flat(); - const sfdxVersions = bodies - .map((body) => [...body.matchAll(sfdxVersionRegex)].map((match) => match[1])) - .flat(); - const nodeVersions = bodies - .map((body) => [...body.matchAll(nodeVersionRegex)].map((match) => match[1])) - .flat(); - console.log("sfVersions", sfVersions); - console.log("sfdxVersions", sfdxVersions); - console.log("nodeVersions", nodeVersions); - if ((sfVersions.length > 0 || sfdxVersions.length > 0)) { - if (sfVersions.length > 0) { - const oneSatisfies = sfVersions.some((version) => semver.gte(version, '2.0.0')); - if (!oneSatisfies) { - // If not, share deprecation information - const sfV1 = getFile("../../messages/deprecated-cli.md", { - THE_AUTHOR: author, - OLD_CLI: "`sf` (v1)", - }); - postComment(sfV1); - cliValid = false; - } - } - if (sfdxVersions.find((v) => v.startsWith("7.")) && - !sfVersions.find((v) => v.startsWith("2."))) { - const noOldSfdx = getFile("../../messages/deprecated-cli.md", { - THE_AUTHOR: author, - OLD_CLI: "`sfdx` (v7)", - }); - postComment(noOldSfdx); - cliValid = false; - } - if (nodeVersions.length > 0) { - if (!(await (0, nodeVersions_1.isAnyVersionValid)(new Date())(nodeVersions))) { - const nodeVersionMessage = getFile("../../messages/unsupported-node.md", { - THE_AUTHOR: author, - NODE_VERSION: nodeVersions.join("`, `"), - }); - postComment(nodeVersionMessage); - closeIssue(); - cliValid = false; - } - } - if (cliValid) { - console.log("A valid CLI version is provided!"); - } - else { - console.log("Information provided is NOT valid"); - addLabel("missing required information"); - } - } - else { - console.log("Full version information was not provided"); - if (!provideVersionAlreadyRequested) { - const message = getFile("../../messages/provide-version.md", { - THE_AUTHOR: issue.user.login, - }); - postComment(message); - provideVersionAlreadyRequested = true; - addLabel("missing required information"); - } - cliValid = false; - } - if (extensionsValid && vscodeValid && osVersionValid && cliValid && lastWorkingVersionValid) { - addLabel("validated"); - removeLabel("missing required information"); - } - else { - console.log("You have one or more missing/invalid versions."); - addLabel("missing required information"); - } + extensionsValid = false; + } + // Checking VSCode version + const vscodeVersionRegex = + /(?:\*{2}VS Code version\*{2}:\s*(?:Version:\s*)?v?(1\.\d{2}\.\d))|(?:VS Code version:\s*(?:Version:\s*)?v?(1\.\d{2}\.\d))/g; + // Search all bodies and get an array of all versions found (first or second capture group) + const vscodeVersions = bodies + .map(body => [...body.matchAll(vscodeVersionRegex)].map(match => match[1] || match[2])) + .flat(); + console.log('vscodeVersions', vscodeVersions); + if (vscodeVersions.length > 0) { + const vscodeMinVersion = getMinimumVSCodeVersion(); + console.log('vscodeMinVersion', vscodeMinVersion); + const oneSatisfies = vscodeVersions.some(version => semver.gte(version, vscodeMinVersion)); + if (!oneSatisfies) { + const oldVSCode = getFile('../../messages/unsupported-vscode.md', { + THE_AUTHOR: author, + USER_VERSION: vscodeVersions.join('`, `'), + MIN_VERSION: vscodeMinVersion + }); + postComment(oldVSCode); + vscodeValid = false; } - // --------- - // FUNCTIONS - // --------- - async function closeIssue() { - return await octokit.rest.issues.update({ - owner, - repo, - issue_number, - state: "closed", - }); + if (vscodeValid) { + console.log('A valid VSCode version is provided!'); + } else { + console.log('The VSCode version provided is NOT valid'); + addLabel('missing required information'); } - async function getAllComments() { - return await octokit.rest.issues.listComments({ - owner, - repo, - issue_number, - }); + } else { + console.log('VSCode version is NOT provided'); + if (!provideVersionAlreadyRequested) { + const message = getFile('../../messages/provide-version.md', { + THE_AUTHOR: issue.user.login + }); + postComment(message); + provideVersionAlreadyRequested = true; + addLabel('missing required information'); } - async function postComment(body) { - // Check that this comment has not been previously commented - if (comments.length) { - if (comments.some((comment) => comment.body === body)) { - console.log("Already commented"); - return; - } - } - return await octokit.rest.issues.createComment({ - owner, - repo, - issue_number, - body, - }); + vscodeValid = false; + } + // Checking presence of OS and version + // NOTE: negative lookahead used in this regex due to false match when OS and version is blank + const osVersionRegex = + /(?:\*{2}OS and version\*{2}:\s*(?!\*\*VS|VS)\S.*?)(?=\r?\n|$)|(?:OS and version:\s*(?!\*\*VS|VS)\S.*?)(?=\r?\n|$)/g; + // Search all bodies and get an array of all versions found (first or second capture group) + const osVersions = bodies + .map(body => [...body.matchAll(osVersionRegex)].map(match => match[1] || match[2])) + .flat(); + if (osVersions.length > 0) { + console.log('OS and version is provided!'); + } else { + console.log('OS and version is NOT provided'); + if (!provideVersionAlreadyRequested) { + const message = getFile('../../messages/provide-version.md', { + THE_AUTHOR: issue.user.login + }); + postComment(message); + provideVersionAlreadyRequested = true; + addLabel('missing required information'); + } + osVersionValid = false; + } + // Checking presence of last working extensions version + const lastWorkingVersionRegex = + /(\*{2}Most recent version of the extensions where this was working\*{2}:\s*\S.*\r\n)|(Most recent version of the extensions where this was working:\s*\S.*\r\n)|(\*{2}Most recent version of the extensions where this was working\*{2}:\s*\S.*$)|(Most recent version of the extensions where this was working:\s*\S.*$)/g; + // Search all bodies and get an array of all versions found (first or second capture group) + const lastWorkingVersions = bodies + .map(body => [...body.matchAll(lastWorkingVersionRegex)].map(match => match[1] || match[2])) + .flat(); + if (lastWorkingVersions.length > 0) { + console.log('Last working version is provided!'); + } else { + console.log('Last working version is NOT provided'); + if (!provideVersionAlreadyRequested) { + const message = getFile('../../messages/provide-version.md', { + THE_AUTHOR: issue.user.login + }); + postComment(message); + provideVersionAlreadyRequested = true; + addLabel('missing required information'); } - async function addLabel(label) { - await octokit.rest.issues.addLabels({ - owner, - repo, - issue_number, - labels: [label], + lastWorkingVersionValid = false; + } + // *** The below is the check for CLI version, code reused from CLI Team's repo *** + const sfVersionRegex = + /(?:Salesforce CLI Version|(?:\*{2}Salesforce CLI Version\*{2})):\s*(?:@salesforce\/cli\/)?(\d+\.\d+\.\d+)/g; + const sfdxVersionRegex = + /(?:Salesforce CLI Version|(?:\*{2}Salesforce CLI Version\*{2})):\s*(?:sfdx-cli\/)?(\d+\.\d+\.\d+)/g; + const nodeVersionRegex = /node-v(\d{2})\.\d+\.\d+/g; + // Search all bodies and get an array of all versions found (first capture group) + const sfVersions = bodies.map(body => [...body.matchAll(sfVersionRegex)].map(match => match[1])).flat(); + const sfdxVersions = bodies.map(body => [...body.matchAll(sfdxVersionRegex)].map(match => match[1])).flat(); + const nodeVersions = bodies.map(body => [...body.matchAll(nodeVersionRegex)].map(match => match[1])).flat(); + console.log('sfVersions', sfVersions); + console.log('sfdxVersions', sfdxVersions); + console.log('nodeVersions', nodeVersions); + if (sfVersions.length > 0 || sfdxVersions.length > 0) { + if (sfVersions.length > 0) { + const oneSatisfies = sfVersions.some(version => semver.gte(version, '2.0.0')); + if (!oneSatisfies) { + // If not, share deprecation information + const sfV1 = getFile('../../messages/deprecated-cli.md', { + THE_AUTHOR: author, + OLD_CLI: '`sf` (v1)' }); + postComment(sfV1); + cliValid = false; + } } - async function removeLabel(label) { - try { - await octokit.rest.issues.removeLabel({ - owner, - repo, - issue_number, - name: label, - }); - } - catch (err) { - const error = err; - if (error.status === 404) { - console.log(`Cannot remove label '${label}' since it was not applied`); - return; - } - throw error; - } + if (sfdxVersions.find(v => v.startsWith('7.')) && !sfVersions.find(v => v.startsWith('2.'))) { + const noOldSfdx = getFile('../../messages/deprecated-cli.md', { + THE_AUTHOR: author, + OLD_CLI: '`sfdx` (v7)' + }); + postComment(noOldSfdx); + cliValid = false; } - function getLatestExtensionsVersion() { - const result = (0, child_process_1.execSync)(`npx vsce show salesforce.salesforcedx-vscode --json`).toString(); - return JSON.parse(result).versions[0].version; + if (nodeVersions.length > 0) { + if (!(await (0, nodeVersions_1.isAnyVersionValid)(new Date())(nodeVersions))) { + const nodeVersionMessage = getFile('../../messages/unsupported-node.md', { + THE_AUTHOR: author, + NODE_VERSION: nodeVersions.join('`, `') + }); + postComment(nodeVersionMessage); + closeIssue(); + cliValid = false; + } } - function getMinimumVSCodeVersion() { - const currentDirectory = (0, child_process_1.execSync)(`pwd`).toString(); - // currentDirectory contains a newline at the end - const packageJsonDirectory = currentDirectory.slice(0, -1) + "/packages/salesforcedx-vscode-core/package.json"; - const packageJsonContent = (0, fs_1.readFileSync)(packageJsonDirectory, 'utf8'); - // The VSCode version has a carat in front that needs to be removed - return JSON.parse(packageJsonContent).engines.vscode.substring(1); + if (cliValid) { + console.log('A valid CLI version is provided!'); + } else { + console.log('Information provided is NOT valid'); + addLabel('missing required information'); } - function getFile(filename, replacements) { - let contents = (0, fs_1.readFileSync)(path.join(__dirname, filename), "utf8"); - Object.entries(replacements || {}).map(([key, value]) => { - contents = contents.replaceAll(key, value); - }); - return contents; + } else { + console.log('Full version information was not provided'); + if (!provideVersionAlreadyRequested) { + const message = getFile('../../messages/provide-version.md', { + THE_AUTHOR: issue.user.login + }); + postComment(message); + provideVersionAlreadyRequested = true; + addLabel('missing required information'); } + cliValid = false; + } + if (extensionsValid && vscodeValid && osVersionValid && cliValid && lastWorkingVersionValid) { + addLabel('validated'); + removeLabel('missing required information'); + } else { + console.log('You have one or more missing/invalid versions.'); + addLabel('missing required information'); + } } - catch (err) { + // --------- + // FUNCTIONS + // --------- + async function closeIssue() { + return await octokit.rest.issues.update({ + owner, + repo, + issue_number, + state: 'closed' + }); + } + async function getAllComments() { + return await octokit.rest.issues.listComments({ + owner, + repo, + issue_number + }); + } + async function postComment(body) { + // Check that this comment has not been previously commented + if (comments.length) { + if (comments.some(comment => comment.body === body)) { + console.log('Already commented'); + return; + } + } + return await octokit.rest.issues.createComment({ + owner, + repo, + issue_number, + body + }); + } + async function addLabel(label) { + await octokit.rest.issues.addLabels({ + owner, + repo, + issue_number, + labels: [label] + }); + } + async function removeLabel(label) { + try { + await octokit.rest.issues.removeLabel({ + owner, + repo, + issue_number, + name: label + }); + } catch (err) { const error = err; - (0, core_1.setFailed)(error.message); + if (error.status === 404) { + console.log(`Cannot remove label '${label}' since it was not applied`); + return; + } + throw error; + } + } + function getLatestExtensionsVersion() { + const result = (0, child_process_1.execSync)(`npx vsce show salesforce.salesforcedx-vscode --json`).toString(); + return JSON.parse(result).versions[0].version; + } + function getMinimumVSCodeVersion() { + const currentDirectory = (0, child_process_1.execSync)(`pwd`).toString(); + // currentDirectory contains a newline at the end + const packageJsonDirectory = currentDirectory.slice(0, -1) + '/packages/salesforcedx-vscode-core/package.json'; + const packageJsonContent = (0, fs_1.readFileSync)(packageJsonDirectory, 'utf8'); + // The VSCode version has a carat in front that needs to be removed + return JSON.parse(packageJsonContent).engines.vscode.substring(1); + } + function getFile(filename, replacements) { + let contents = (0, fs_1.readFileSync)(path.join(__dirname, filename), 'utf8'); + Object.entries(replacements || {}).map(([key, value]) => { + contents = contents.replaceAll(key, value); + }); + return contents; } + } catch (err) { + const error = err; + (0, core_1.setFailed)(error.message); + } } run(); -//# sourceMappingURL=index.js.map \ No newline at end of file +//# sourceMappingURL=index.js.map diff --git a/.github/actions/validate-issue/lib/src/nodeVersions.js b/.github/actions/validate-issue/lib/src/nodeVersions.js index ee201e7226..dacd3c2c58 100644 --- a/.github/actions/validate-issue/lib/src/nodeVersions.js +++ b/.github/actions/validate-issue/lib/src/nodeVersions.js @@ -1,13 +1,16 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); +'use strict'; +Object.defineProperty(exports, '__esModule', { value: true }); exports.isAnyVersionValid = void 0; -const isAnyVersionValid = (currentDate) => async (versions) => { - const resp = (await (await fetch("https://raw.githubusercontent.com/nodejs/Release/main/schedule.json")).json()); - return versions - .map((version) => `v${version}`) - .some((formattedVersion) => formattedVersion in resp && +const isAnyVersionValid = currentDate => async versions => { + const resp = await (await fetch('https://raw.githubusercontent.com/nodejs/Release/main/schedule.json')).json(); + return versions + .map(version => `v${version}`) + .some( + formattedVersion => + formattedVersion in resp && currentDate >= new Date(resp[formattedVersion].start) && - currentDate <= new Date(resp[formattedVersion].end)); + currentDate <= new Date(resp[formattedVersion].end) + ); }; exports.isAnyVersionValid = isAnyVersionValid; -//# sourceMappingURL=nodeVersions.js.map \ No newline at end of file +//# sourceMappingURL=nodeVersions.js.map From b2f92d0d86240c9f1e767a0aaacf5b32b6a256c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristina=20Isabel=20Ca=C3=B1izales?= Date: Wed, 20 Nov 2024 20:10:37 -0300 Subject: [PATCH 7/7] chore: add sourceUri param --- .../salesforcedx-vscode-apex/package.json | 6 +++ .../src/commands/apexActionController.ts | 5 +-- .../src/commands/createApexAction.ts | 5 ++- .../src/commands/metadataOrchestrator.ts | 41 +++++++++++++------ .../commands/metadataOrchestrator.test.ts | 12 +++--- 5 files changed, 45 insertions(+), 24 deletions(-) diff --git a/packages/salesforcedx-vscode-apex/package.json b/packages/salesforcedx-vscode-apex/package.json index b47c165945..962bf3724f 100644 --- a/packages/salesforcedx-vscode-apex/package.json +++ b/packages/salesforcedx-vscode-apex/package.json @@ -136,6 +136,12 @@ "when": "sf:project_opened && sf:has_target_org && resource =~ /.\\.(cls)?$/" } ], + "explorer/context": [ + { + "command": "sf.create.apex.action.class", + "when": "sf:project_opened && sf:has_target_org && resource =~ /.\\.(cls)?$/ && resourcePath =~ /classes/" + } + ], "view/title": [ { "command": "sf.test.view.run", diff --git a/packages/salesforcedx-vscode-apex/src/commands/apexActionController.ts b/packages/salesforcedx-vscode-apex/src/commands/apexActionController.ts index 7f1d7d6b10..1d7e9a71a2 100644 --- a/packages/salesforcedx-vscode-apex/src/commands/apexActionController.ts +++ b/packages/salesforcedx-vscode-apex/src/commands/apexActionController.ts @@ -4,7 +4,6 @@ * Licensed under the BSD 3-Clause license. * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ -import { Progress } from '@salesforce/apex-node-bundle'; import { notificationService, workspaceUtils } from '@salesforce/salesforcedx-utils-vscode'; import * as fs from 'fs'; import { OpenAPIV3 } from 'openapi-types'; @@ -22,7 +21,7 @@ export class ApexActionController { * Creates an Apex Action. * @param isClass - Indicates if the action is for a class or a method. */ - public createApexAction = async (isClass: boolean): Promise => { + public createApexAction = async (isClass: boolean, sourceUri?: vscode.Uri): Promise => { const type = isClass ? 'Class' : 'Method'; const command = isClass ? 'SFDX: Create Apex Action from This Class' @@ -41,7 +40,7 @@ export class ApexActionController { // Step 1: Extract Metadata progress.report({ message: nls.localize('extract_metadata') }); metadata = isClass - ? this.metadataOrchestrator.extractAllMethodsMetadata() + ? await this.metadataOrchestrator.extractAllMethodsMetadata(sourceUri) : this.metadataOrchestrator.extractMethodMetadata(); if (!metadata) { throw new Error(nls.localize('extraction_failed', type)); diff --git a/packages/salesforcedx-vscode-apex/src/commands/createApexAction.ts b/packages/salesforcedx-vscode-apex/src/commands/createApexAction.ts index f37608f41c..e69b84af99 100644 --- a/packages/salesforcedx-vscode-apex/src/commands/createApexAction.ts +++ b/packages/salesforcedx-vscode-apex/src/commands/createApexAction.ts @@ -4,6 +4,7 @@ * Licensed under the BSD 3-Clause license. * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ +import * as vscode from 'vscode'; import { ApexActionController } from './apexActionController'; import { MetadataOrchestrator } from './metadataOrchestrator'; @@ -21,7 +22,7 @@ export const createApexActionFromMethod = async (): Promise => { /** * Creates Apex Actions from all methods in the current class. */ -export const createApexActionFromClass = async (): Promise => { +export const createApexActionFromClass = async (sourceUri: vscode.Uri | undefined): Promise => { // Call Controller - await controller.createApexAction(true); + await controller.createApexAction(true, sourceUri); }; diff --git a/packages/salesforcedx-vscode-apex/src/commands/metadataOrchestrator.ts b/packages/salesforcedx-vscode-apex/src/commands/metadataOrchestrator.ts index be266bfeda..cd2f850341 100644 --- a/packages/salesforcedx-vscode-apex/src/commands/metadataOrchestrator.ts +++ b/packages/salesforcedx-vscode-apex/src/commands/metadataOrchestrator.ts @@ -86,20 +86,35 @@ export class MetadataOrchestrator { return methodMetadata; }; - public extractAllMethodsMetadata = (): MethodMetadata[] | undefined => { - const editor = vscode.window.activeTextEditor; - if (!editor) { - notificationService.showErrorMessage(nls.localize('no_active_editor')); - return; - } + public extractAllMethodsMetadata = async ( + sourceUri: vscode.Uri | undefined + ): Promise => { + let lines; + let className; + if (sourceUri) { + const path = sourceUri?.path.toString(); + className = path! + .substring(path!.lastIndexOf(process.platform === 'win32' ? '\\' : '/') + 1) + .split('.') + .shift(); + const fileContent = await vscode.workspace.fs.readFile(sourceUri!); + const fileText = Buffer.from(fileContent).toString('utf-8'); + lines = fileText.split('\n'); + } else { + const editor = vscode.window.activeTextEditor; + if (!editor) { + notificationService.showErrorMessage(nls.localize('no_active_editor')); + return; + } - const document = editor.document; - const filePath = document.fileName; - const className = filePath - .substring(filePath.lastIndexOf(process.platform === 'win32' ? '\\' : '/') + 1) - .split('.') - .shift(); - const lines = document.getText().split('\n'); + const document = editor.document; + const filePath = document.fileName; + className = filePath + .substring(filePath.lastIndexOf(process.platform === 'win32' ? '\\' : '/') + 1) + .split('.') + .shift(); + lines = document.getText().split('\n'); + } const metadataList: MethodMetadata[] = []; let currentMethodSignature = ''; let isAuraEnabled = false; diff --git a/packages/salesforcedx-vscode-apex/test/jest/commands/metadataOrchestrator.test.ts b/packages/salesforcedx-vscode-apex/test/jest/commands/metadataOrchestrator.test.ts index a3e879d37e..f5ec4d18e8 100644 --- a/packages/salesforcedx-vscode-apex/test/jest/commands/metadataOrchestrator.test.ts +++ b/packages/salesforcedx-vscode-apex/test/jest/commands/metadataOrchestrator.test.ts @@ -83,14 +83,14 @@ describe('MetadataOrchestrator', () => { }); describe('extractAllMethodsMetadata', () => { - it('should return undefined if no active editor', () => { + it('should return undefined if no active editor', async () => { (vscode.window as any).activeTextEditor = undefined; - const result = orchestrator.extractAllMethodsMetadata(); + const result = await orchestrator.extractAllMethodsMetadata(undefined); expect(result).toBeUndefined(); expect(showErrorMessageMock).toHaveBeenCalledTimes(1); }); - it('should return metadata for all methods', () => { + it('should return metadata for all methods from active editor', async () => { const editorStub = { document: { uri: { path: 'someClass.cls' } as vscode.Uri, @@ -103,7 +103,7 @@ describe('MetadataOrchestrator', () => { } as vscode.TextEditor; (vscode.window as any).activeTextEditor = editorStub; - const result = orchestrator.extractAllMethodsMetadata(); + const result = await orchestrator.extractAllMethodsMetadata(undefined); expect(result).toEqual([ { name: 'methodOne', @@ -122,7 +122,7 @@ describe('MetadataOrchestrator', () => { ]); }); - it('should throw an error if no eligible methods are found', () => { + it('should throw an error if no eligible methods are found', async () => { const editorStub = { document: { uri: { path: 'someClass.cls' } as vscode.Uri, @@ -135,7 +135,7 @@ describe('MetadataOrchestrator', () => { } as vscode.TextEditor; (vscode.window as any).activeTextEditor = editorStub; - expect(() => orchestrator.extractAllMethodsMetadata()).toThrow(); + await expect(() => orchestrator.extractAllMethodsMetadata(undefined)).rejects.toThrow(); }); });