Skip to content

Commit

Permalink
Merge pull request #877 from salesforcecli/sm/set-multiple-psl
Browse files Browse the repository at this point in the history
fix: set multiple psl
  • Loading branch information
shetzel authored Feb 26, 2024
2 parents 3a6129c + 646904b commit d1c92ff
Show file tree
Hide file tree
Showing 5 changed files with 176 additions and 128 deletions.
231 changes: 117 additions & 114 deletions src/baseCommands/user/permsetlicense/assign.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/

import { Connection, Logger, Messages, SfError, StateAggregator } from '@salesforce/core';
import { SfCommand } from '@salesforce/sf-plugins-core';
import { Connection, Lifecycle, Logger, Messages, SfError, StateAggregator } from '@salesforce/core';
import { Ux } from '@salesforce/sf-plugins-core';

Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
const messages = Messages.loadMessages('@salesforce/plugin-user', 'permsetlicense.assign');
Expand All @@ -30,141 +30,144 @@ interface PermissionSetLicense {
Id: string;
}

export abstract class UserPermSetLicenseAssignBaseCommand extends SfCommand<PSLResult> {
private readonly successes: SuccessMsg[] = [];
private readonly failures: FailureMsg[] = [];

public async assign({
conn,
pslName,
usernamesOrAliases,
}: {
conn: Connection;
pslName: string;
usernamesOrAliases: string[];
}): Promise<PSLResult> {
const logger = await Logger.child(this.constructor.name);

logger.debug(`will assign perm set license "${pslName}" to users: ${usernamesOrAliases.join(', ')}`);
const pslId = await queryPsl(conn, pslName);

(
await Promise.all(
usernamesOrAliases.map((usernameOrAlias) =>
this.usernameToPSLAssignment({
pslName,
usernameOrAlias,
pslId,
conn,
})
export const assignPSL = async ({
conn,
pslName,
usernamesOrAliases,
}: {
conn: Connection;
pslName: string;
usernamesOrAliases: string[];
}): Promise<PSLResult> => {
const logger = await Logger.child('assignPSL');

logger.debug(`will assign perm set license "${pslName}" to users: ${usernamesOrAliases.join(', ')}`);
const queryResult = await queryPsl({ conn, pslName, usernamesOrAliases });

return typeof queryResult === 'string'
? aggregate(
await Promise.all(
usernamesOrAliases.map((usernameOrAlias) =>
usernameToPSLAssignment({
pslName,
usernameOrAlias,
pslId: queryResult,
conn,
})
)
)
)
).map((result) => {
if (isSuccess(result)) {
this.successes.push(result);
} else {
this.failures.push(result);
}
});

this.print();
this.setExitCode();
: queryResult;
};

return {
successes: this.successes,
failures: this.failures,
};
/** reduce an array of PSLResult to a single one */
export const aggregate = (results: PSLResult[]): PSLResult => ({
successes: results.flatMap((r) => r.successes),
failures: results.flatMap((r) => r.failures),
});

export const resultsToExitCode = (results: PSLResult): number => {
if (results.failures.length && results.successes.length) {
return 68;
} else if (results.failures.length) {
return 1;
} else if (results.successes.length) {
return 0;
}
throw new SfError('Unexpected state: no successes and no failures. This should not happen.');
};

// handles one username/psl combo so these can run in parallel
private async usernameToPSLAssignment({
pslName,
usernameOrAlias,
pslId,
conn,
}: {
pslName: string;
usernameOrAlias: string;
pslId: string;
conn: Connection;
}): Promise<SuccessMsg | FailureMsg> {
// Convert any aliases to usernames
const resolvedUsername = (await StateAggregator.getInstance()).aliases.resolveUsername(usernameOrAlias);

try {
const AssigneeId = (
await conn.singleRecordQuery<{ Id: string }>(`select Id from User where Username = '${resolvedUsername}'`)
).Id;

await conn.sobject('PermissionSetLicenseAssign').create({
AssigneeId,
PermissionSetLicenseId: pslId,
});
return {
name: resolvedUsername,
value: pslName,
};
} catch (e) {
// idempotency. If user(s) already have PSL, the API will throw an error about duplicate value.
// but we're going to call that a success
if (e instanceof Error && e.message.startsWith('duplicate value found')) {
this.warn(messages.getMessage('duplicateValue', [resolvedUsername, pslName]));
return {
name: resolvedUsername,
value: pslName,
};
} else {
return {
name: resolvedUsername,
message: e instanceof Error ? e.message : 'error contained no message',
};
}
}
export const print = (results: PSLResult): void => {
const ux = new Ux();
if (results.failures.length > 0 && results.successes.length > 0) {
ux.styledHeader('Partial Success');
}
if (results.successes.length > 0) {
ux.styledHeader('Permset Licenses Assigned');
ux.table(results.successes, {
name: { header: 'Username' },
value: { header: 'Permission Set License Assignment' },
});
}

private setExitCode(): void {
if (this.failures.length && this.successes.length) {
process.exitCode = 68;
} else if (this.failures.length) {
process.exitCode = 1;
} else if (this.successes.length) {
process.exitCode = 0;
if (results.failures.length > 0) {
if (results.successes.length > 0) {
ux.log('');
}

ux.styledHeader('Failures');
ux.table(results.failures, { name: { header: 'Username' }, message: { header: 'Error Message' } });
}
};

private print(): void {
if (this.failures.length > 0 && this.successes.length > 0) {
this.styledHeader('Partial Success');
}
if (this.successes.length > 0) {
this.styledHeader('Permset Licenses Assigned');
this.table(this.successes, {
name: { header: 'Username' },
value: { header: 'Permission Set License Assignment' },
});
}
// handles one username/psl combo so these can run in parallel
const usernameToPSLAssignment = async ({
pslName,
usernameOrAlias,
pslId,
conn,
}: {
pslName: string;
usernameOrAlias: string;
pslId: string;
conn: Connection;
}): Promise<PSLResult> => {
// Convert any aliases to usernames
const resolvedUsername = (await StateAggregator.getInstance()).aliases.resolveUsername(usernameOrAlias);

if (this.failures.length > 0) {
if (this.successes.length > 0) {
this.log('');
}
try {
const AssigneeId = (
await conn.singleRecordQuery<{ Id: string }>(`select Id from User where Username = '${resolvedUsername}'`)
).Id;

this.styledHeader('Failures');
this.table(this.failures, { name: { header: 'Username' } }, { message: { header: 'Error Message' } });
await conn.sobject('PermissionSetLicenseAssign').create({
AssigneeId,
PermissionSetLicenseId: pslId,
});
return toResult({
name: resolvedUsername,
value: pslName,
});
} catch (e) {
// idempotency. If user(s) already have PSL, the API will throw an error about duplicate value.
// but we're going to call that a success
if (e instanceof Error && e.message.startsWith('duplicate value found')) {
await Lifecycle.getInstance().emitWarning(messages.getMessage('duplicateValue', [resolvedUsername, pslName]));
return toResult({
name: resolvedUsername,
value: pslName,
});
} else {
return toResult({
name: resolvedUsername,
message: e instanceof Error ? e.message : 'error contained no message',
});
}
}
}
};

const toResult = (input: SuccessMsg | FailureMsg): PSLResult =>
isSuccess(input) ? { successes: [input], failures: [] } : { successes: [], failures: [input] };
const isSuccess = (input: SuccessMsg | FailureMsg): input is SuccessMsg => (input as SuccessMsg).value !== undefined;

const queryPsl = async (conn: Connection, pslName: string): Promise<string> => {
const queryPsl = async ({
conn,
pslName,
usernamesOrAliases,
}: {
conn: Connection;
pslName: string;
usernamesOrAliases: string[];
}): Promise<string | PSLResult> => {
try {
return (
await conn.singleRecordQuery<PermissionSetLicense>(
`select Id from PermissionSetLicense where DeveloperName = '${pslName}' or MasterLabel = '${pslName}'`
)
).Id;
} catch (e) {
throw new SfError('PermissionSetLicense not found');
return aggregate(
usernamesOrAliases.map((name) => toResult({ name, message: `PermissionSetLicense not found: ${pslName}` }))
);
}
};
19 changes: 15 additions & 4 deletions src/commands/force/user/permsetlicense/assign.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,20 @@
*/

import { Messages } from '@salesforce/core';
import { arrayWithDeprecation, Flags, loglevel, orgApiVersionFlagWithDeprecations } from '@salesforce/sf-plugins-core';
import {
arrayWithDeprecation,
Flags,
loglevel,
orgApiVersionFlagWithDeprecations,
SfCommand,
} from '@salesforce/sf-plugins-core';
import { ensureArray } from '@salesforce/kit';
import { PSLResult, UserPermSetLicenseAssignBaseCommand } from '../../../../baseCommands/user/permsetlicense/assign.js';
import { assignPSL, print, PSLResult, resultsToExitCode } from '../../../../baseCommands/user/permsetlicense/assign.js';

Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
const messages = Messages.loadMessages('@salesforce/plugin-user', 'permsetlicense.assign');

export class ForceUserPermSetLicenseAssignCommand extends UserPermSetLicenseAssignBaseCommand {
export class ForceUserPermSetLicenseAssignCommand extends SfCommand<PSLResult> {
public static readonly hidden = true;
public static readonly summary = messages.getMessage('summary');
public static readonly description = messages.getMessage('description');
Expand Down Expand Up @@ -48,10 +54,15 @@ export class ForceUserPermSetLicenseAssignCommand extends UserPermSetLicenseAssi

public async run(): Promise<PSLResult> {
const { flags } = await this.parse(ForceUserPermSetLicenseAssignCommand);
return this.assign({
const result = await assignPSL({
conn: flags['target-org'].getConnection(flags['api-version']),
pslName: flags.name,
usernamesOrAliases: ensureArray(flags['on-behalf-of'] ?? flags['target-org'].getUsername()),
});
process.exitCode = resultsToExitCode(result);
if (!this.jsonEnabled()) {
print(result);
}
return result;
}
}
35 changes: 27 additions & 8 deletions src/commands/org/assign/permsetlicense.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,20 @@
*/

import { Messages } from '@salesforce/core';
import { arrayWithDeprecation, Flags } from '@salesforce/sf-plugins-core';
import { arrayWithDeprecation, Flags, SfCommand } from '@salesforce/sf-plugins-core';
import { ensureArray } from '@salesforce/kit';
import { PSLResult, UserPermSetLicenseAssignBaseCommand } from '../../../baseCommands/user/permsetlicense/assign.js';
import {
aggregate,
assignPSL,
print,
PSLResult,
resultsToExitCode,
} from '../../../baseCommands/user/permsetlicense/assign.js';

Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
const messages = Messages.loadMessages('@salesforce/plugin-user', 'permsetlicense.assign');

export class AssignPermSetLicenseCommand extends UserPermSetLicenseAssignBaseCommand {
export class AssignPermSetLicenseCommand extends SfCommand<PSLResult> {
public static readonly summary = messages.getMessage('summary');
public static readonly description = messages.getMessage('description');
public static readonly examples = messages.getMessages('examples');
Expand All @@ -23,6 +29,7 @@ export class AssignPermSetLicenseCommand extends UserPermSetLicenseAssignBaseCom
summary: messages.getMessage('flags.name.summary'),
required: true,
aliases: ['perm-set-license', 'psl'],
multiple: true,
}),
'on-behalf-of': arrayWithDeprecation({
char: 'b',
Expand All @@ -36,10 +43,22 @@ export class AssignPermSetLicenseCommand extends UserPermSetLicenseAssignBaseCom

public async run(): Promise<PSLResult> {
const { flags } = await this.parse(AssignPermSetLicenseCommand);
return this.assign({
conn: flags['target-org'].getConnection(flags['api-version']),
pslName: flags.name,
usernamesOrAliases: ensureArray(flags['on-behalf-of'] ?? flags['target-org'].getUsername()),
});
const result = aggregate(
await Promise.all(
flags.name.map((pslName) =>
assignPSL({
conn: flags['target-org'].getConnection(flags['api-version']),
pslName,
usernamesOrAliases: ensureArray(flags['on-behalf-of'] ?? flags['target-org'].getUsername()),
})
)
)
);

process.exitCode = resultsToExitCode(result);
if (!this.jsonEnabled()) {
print(result);
}
return result;
}
}
17 changes: 16 additions & 1 deletion test/commands/permsetlicense/assign.nut.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,22 @@ describe('PermissionSetLicense tests', () => {
});
});

describe('multiple PSL via onBehalfOf', () => {
describe('assign multiple PSLs to default user', () => {
it('2 successful assignments', () => {
const consolePSL = 'Sales Console User';
const commandResult = execCmd<PSLResult>(`org:assign:permsetlicense -n ${testPSL} -n "${consolePSL}" --json`, {
ensureExitCode: 0,
}).jsonOutput as { status: number; result: PSLResult; warnings: string[] };
expect(commandResult.result.failures).to.be.an('array').with.length(0);
expect(commandResult.result.successes.some((success) => success.value === testPSL)).to.be.true;
expect(commandResult.result.successes.some((success) => success.value === consolePSL)).to.be.true;
// warning because already assigned
expect(commandResult.warnings).to.be.an('array').with.length(1);
expect(commandResult.warnings[0]).to.include(testPSL);
});
});

describe('assign PSL to multiple users via onBehalfOf', () => {
it('assigns a psl to multiple users via onBehalfOf', () => {
const anotherPSL = 'SurveyCreatorPsl';
const originalUsername = session.orgs.get('default')?.username;
Expand Down
Loading

0 comments on commit d1c92ff

Please sign in to comment.