From fecd055b677058e75a9fc0e4f6a30808e4d182ad Mon Sep 17 00:00:00 2001 From: readeral Date: Tue, 27 Aug 2024 09:25:52 +1000 Subject: [PATCH 1/5] Initial unlocked packages command --- src/common/utils/apiUtils.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/common/utils/apiUtils.ts b/src/common/utils/apiUtils.ts index 0f8f6f353..29ae787a0 100644 --- a/src/common/utils/apiUtils.ts +++ b/src/common/utils/apiUtils.ts @@ -19,6 +19,11 @@ export function soqlQueryTooling(soqlQuery: string, conn: Connection): Promise void): Promise { + return conn.tooling.describeGlobal(callback); +} + let spinnerQ; const maxRetry = Number(process.env.BULK_QUERY_RETRY || 5); // Same than soqlQuery but using bulk. Do not use if there will be too many results for javascript to handle in memory From f0c43c695d776a7150d46db517386b80876754ac Mon Sep 17 00:00:00 2001 From: readeral Date: Tue, 27 Aug 2024 09:26:36 +1000 Subject: [PATCH 2/5] Initial unlocked packages command --- .../hardis/project/clean/unlockedpackages.ts | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 src/commands/hardis/project/clean/unlockedpackages.ts diff --git a/src/commands/hardis/project/clean/unlockedpackages.ts b/src/commands/hardis/project/clean/unlockedpackages.ts new file mode 100644 index 000000000..b6b139e34 --- /dev/null +++ b/src/commands/hardis/project/clean/unlockedpackages.ts @@ -0,0 +1,122 @@ +/* jscpd:ignore-start */ +import { flags, SfdxCommand } from "@salesforce/command"; +import { Messages } from "@salesforce/core"; +import { AnyJson } from "@salesforce/ts-types"; +import * as c from "chalk"; +import { uxLog } from "../../../../common/utils"; +import { soqlQueryTooling, describeGlobalTooling } from "../../../../common/utils/apiUtils"; +import { prompts } from "../../../../common/utils/prompts"; + +// Initialize Messages with the current plugin directory +Messages.importMessagesDirectory(__dirname); + +// Load the specific messages for this file. Messages from @salesforce/command, @salesforce/core, +// or any library that is using the messages framework can also be loaded this way. +const messages = Messages.loadMessages("sfdx-hardis", "org"); + +export default class unlockedpackages extends SfdxCommand { + public static title = "Clean installed unlocked packages"; + + public static description = `Clean installed unlocked packages, such as those installed from unofficialSF`; + + public static examples = ["$ sfdx hardis:project:clean:unlockedpackages"]; + + protected static flagsConfig = { + path: flags.string({ + char: "p", + default: process.cwd(), + description: "Root folder", + }), + debug: flags.boolean({ + char: "d", + default: false, + description: messages.getMessage("debugMode"), + }), + websocket: flags.string({ + description: messages.getMessage("websocket"), + }), + skipauth: flags.boolean({ + description: "Skip authentication check when a default username is required", + }), + }; + + // Comment this out if your command does not require an org username + protected static requiresUsername = true; + + // Comment this out if your command does not support a hub org username + protected static requiresDevhubUsername = false; + + // Set this to true if your command requires a project workspace; 'requiresProject' is false by default + protected static requiresProject = true; + + protected pathToBrowse: string; + protected debugMode = false; + + public async run(): Promise { + this.pathToBrowse = this.flags.path || process.cwd(); + this.debugMode = this.flags.debug || false; + + /* jscpd:ignore-end */ + + // List available unlocked packages in org + const pkgsRequest = "SELECT SubscriberPackageId, SubscriberPackage.NamespacePrefix, SubscriberPackage.Name FROM InstalledSubscriberPackage ORDER BY SubscriberPackage.NamespacePrefix"; + const pkgsResult = await soqlQueryTooling(pkgsRequest, this.org.getConnection()); + const choices = pkgsResult.records + .filter(pkg => pkg.SubscriberPackage.NamespacePrefix == null) + .map((pkg) => ({ + title: pkg.SubscriberPackage.Name, + value: pkg.SubscriberPackageId + }) + ); + + // Get All Org SObject with prefix key + const describeObjResult = await describeGlobalTooling(this.org.getConnection()); + const orgPrefixKey = describeObjResult.sobjects.reduce((obj, item) => ({ + ...obj, + [item.keyPrefix]: item.name + }), {}); + + //Prompt which package to clean up + const promptUlpkgToClean = await prompts([ + { + type: "select", + name: "ulpkg", + message: "Please select the package to clean out", + choices: choices + } + ]) + + const ulpkgToClean = promptUlpkgToClean.ulpkg; + + // Tooling query specific package + const ulpkgRequest = `SELECT SubjectID, SubjectKeyPrefix FROM Package2Member WHERE SubscriberPackageId='${ulpkgToClean}'` + const ulpkgQueryResult = await soqlQueryTooling(ulpkgRequest, this.org.getConnection()); + const memberExceptions =[]; + const ulpkgMembers = ulpkgQueryResult.records.map(member => ({ + SubjectId: member.SubjectId, + SubjectKeyPrefix: member.SubjectKeyPrefix, + ObjectName: orgPrefixKey[member.SubjectKeyPrefix] + })).reduce((acc, { ObjectName, SubjectId }) => { + if (ObjectName) { + acc[ObjectName] = acc[ObjectName] || []; + acc[ObjectName].push(SubjectId); + } else { + memberExceptions.push(SubjectId); + } + return acc; + }, {}); + + console.log(ulpkgMembers); + console.log(memberExceptions); + // Create json file + + // Do Clean + + + // Summary + const msg = `Cleaned ${c.green(c.bold(promptUlpkgToClean.ulpkg[0].title))}.`; + uxLog(this, c.cyan(msg)); + // Return an object to be displayed with --json + return { outputString: msg }; + } +} From 8ab1fd7f70e5ed4bdcb550f9a589b5ab0458c215 Mon Sep 17 00:00:00 2001 From: readeral Date: Tue, 27 Aug 2024 15:39:01 +1000 Subject: [PATCH 3/5] return object of items to delete --- .../hardis/project/clean/unlockedpackages.ts | 46 +++++++++++-------- src/common/utils/apiUtils.ts | 5 ++ 2 files changed, 31 insertions(+), 20 deletions(-) diff --git a/src/commands/hardis/project/clean/unlockedpackages.ts b/src/commands/hardis/project/clean/unlockedpackages.ts index b6b139e34..5e3e4086c 100644 --- a/src/commands/hardis/project/clean/unlockedpackages.ts +++ b/src/commands/hardis/project/clean/unlockedpackages.ts @@ -1,10 +1,10 @@ /* jscpd:ignore-start */ import { flags, SfdxCommand } from "@salesforce/command"; -import { Messages } from "@salesforce/core"; +import { Connection, Messages } from "@salesforce/core"; import { AnyJson } from "@salesforce/ts-types"; import * as c from "chalk"; import { uxLog } from "../../../../common/utils"; -import { soqlQueryTooling, describeGlobalTooling } from "../../../../common/utils/apiUtils"; +import { soqlQueryTooling, describeGlobalTooling, toolingRequest } from "../../../../common/utils/apiUtils"; import { prompts } from "../../../../common/utils/prompts"; // Initialize Messages with the current plugin directory @@ -80,41 +80,47 @@ export default class unlockedpackages extends SfdxCommand { const promptUlpkgToClean = await prompts([ { type: "select", - name: "ulpkg", + name: "packageId", message: "Please select the package to clean out", choices: choices } ]) - const ulpkgToClean = promptUlpkgToClean.ulpkg; + const chosenPackage = choices.filter(id => id.value == promptUlpkgToClean.packageId)[0] // Tooling query specific package - const ulpkgRequest = `SELECT SubjectID, SubjectKeyPrefix FROM Package2Member WHERE SubscriberPackageId='${ulpkgToClean}'` - const ulpkgQueryResult = await soqlQueryTooling(ulpkgRequest, this.org.getConnection()); - const memberExceptions =[]; + const ulpkgQuery = `SELECT SubjectID, SubjectKeyPrefix FROM Package2Member WHERE SubscriberPackageId='${promptUlpkgToClean.packageId}'` + const ulpkgQueryResult = await soqlQueryTooling(ulpkgQuery, this.org.getConnection()); + + //create array of package members, looking up object name from orgPrefixKey const ulpkgMembers = ulpkgQueryResult.records.map(member => ({ SubjectId: member.SubjectId, SubjectKeyPrefix: member.SubjectKeyPrefix, ObjectName: orgPrefixKey[member.SubjectKeyPrefix] - })).reduce((acc, { ObjectName, SubjectId }) => { - if (ObjectName) { - acc[ObjectName] = acc[ObjectName] || []; - acc[ObjectName].push(SubjectId); - } else { - memberExceptions.push(SubjectId); - } - return acc; - }, {}); + })).filter(member => member.ObjectName !== undefined); + + //fetch metadata for package members + const ulpkgMeta = await Promise.all(ulpkgMembers.map(async (member) => { + const toolingQuery: [string, Connection, Record] = [ + `sobjects/${member.ObjectName}/${member.SubjectId}`, + this.org.getConnection(), + {} + ] + const returnResponse: Record = await toolingRequest(...toolingQuery) + return { + name: returnResponse.Name || returnResponse.DeveloperName, + fullName: returnResponse.FullName + } + })); + + console.log(ulpkgMeta) - console.log(ulpkgMembers); - console.log(memberExceptions); // Create json file // Do Clean - // Summary - const msg = `Cleaned ${c.green(c.bold(promptUlpkgToClean.ulpkg[0].title))}.`; + const msg = `Cleaned ${c.green(c.bold(chosenPackage.title))}.`; uxLog(this, c.cyan(msg)); // Return an object to be displayed with --json return { outputString: msg }; diff --git a/src/common/utils/apiUtils.ts b/src/common/utils/apiUtils.ts index 29ae787a0..50ef85e78 100644 --- a/src/common/utils/apiUtils.ts +++ b/src/common/utils/apiUtils.ts @@ -19,6 +19,11 @@ export function soqlQueryTooling(soqlQuery: string, conn: Connection): Promise { + const url = `${conn.instanceUrl}/services/data/v61.0/tooling/${endpoint}` + return conn.tooling.request({url, ...info}); +} + // Perform Tooling Global Description API call export function describeGlobalTooling(conn: Connection, callback?: () => void): Promise { return conn.tooling.describeGlobal(callback); From 2f2d800726649f4bbadac2124282e4311fdcf321 Mon Sep 17 00:00:00 2001 From: readeral Date: Tue, 27 Aug 2024 15:43:44 +1000 Subject: [PATCH 4/5] minor adjustment to cspell --- .github/linters/.cspell.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/linters/.cspell.json b/.github/linters/.cspell.json index a6304cea1..5c3f87feb 100644 --- a/.github/linters/.cspell.json +++ b/.github/linters/.cspell.json @@ -780,6 +780,8 @@ "unfrozeuser", "unicity", "uniquement", + "unlockedpackage", + "ulpkg", "unmanaged", "unpackaged", "unparse", From 80d5c6b6da480c2336d6402aedc22822953ff296 Mon Sep 17 00:00:00 2001 From: readeral Date: Tue, 27 Aug 2024 16:06:50 +1000 Subject: [PATCH 5/5] getting SubscriberPackageVersionId for future validating retained json files --- src/commands/hardis/project/clean/unlockedpackages.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/commands/hardis/project/clean/unlockedpackages.ts b/src/commands/hardis/project/clean/unlockedpackages.ts index 5e3e4086c..47c611d70 100644 --- a/src/commands/hardis/project/clean/unlockedpackages.ts +++ b/src/commands/hardis/project/clean/unlockedpackages.ts @@ -59,13 +59,14 @@ export default class unlockedpackages extends SfdxCommand { /* jscpd:ignore-end */ // List available unlocked packages in org - const pkgsRequest = "SELECT SubscriberPackageId, SubscriberPackage.NamespacePrefix, SubscriberPackage.Name FROM InstalledSubscriberPackage ORDER BY SubscriberPackage.NamespacePrefix"; + const pkgsRequest = "SELECT SubscriberPackageId, SubscriberPackage.NamespacePrefix, SubscriberPackage.Name, SubscriberPackageVersionId FROM InstalledSubscriberPackage ORDER BY SubscriberPackage.NamespacePrefix"; const pkgsResult = await soqlQueryTooling(pkgsRequest, this.org.getConnection()); const choices = pkgsResult.records .filter(pkg => pkg.SubscriberPackage.NamespacePrefix == null) .map((pkg) => ({ title: pkg.SubscriberPackage.Name, - value: pkg.SubscriberPackageId + value: pkg.SubscriberPackageId, + version: pkg.SubscriberPackageVersionId }) ); @@ -91,6 +92,8 @@ export default class unlockedpackages extends SfdxCommand { // Tooling query specific package const ulpkgQuery = `SELECT SubjectID, SubjectKeyPrefix FROM Package2Member WHERE SubscriberPackageId='${promptUlpkgToClean.packageId}'` const ulpkgQueryResult = await soqlQueryTooling(ulpkgQuery, this.org.getConnection()); + + //create array of package members, looking up object name from orgPrefixKey const ulpkgMembers = ulpkgQueryResult.records.map(member => ({