Skip to content

Commit

Permalink
feat: fix Failures table and handle partial success avoiding throws
Browse files Browse the repository at this point in the history
  • Loading branch information
mshanemc committed Feb 23, 2024
1 parent eba69ac commit 4ce8080
Show file tree
Hide file tree
Showing 2 changed files with 51 additions and 39 deletions.
77 changes: 44 additions & 33 deletions src/baseCommands/user/permsetlicense/assign.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,31 +42,30 @@ export const assignPSL = async ({
const logger = await Logger.child('assignPSL');

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

return (
await Promise.all(
usernamesOrAliases.map((usernameOrAlias) =>
usernameToPSLAssignment({
pslName,
usernameOrAlias,
pslId,
conn,
})
const queryResult = await queryPsl({ conn, pslName, usernamesOrAliases });

return typeof queryResult === 'string'
? aggregate(
await Promise.all(
usernamesOrAliases.map((usernameOrAlias) =>
usernameToPSLAssignment({
pslName,
usernameOrAlias,
pslId: queryResult,
conn,
})
)
)
)
)
).reduce<PSLResult>(
(acc, result) =>
isSuccess(result)
? { ...acc, successes: [...acc.successes, result] }
: { ...acc, failures: [...acc.failures, result] },
{
successes: [],
failures: [],
}
);
: queryResult;
};

/** 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;
Expand All @@ -75,7 +74,7 @@ export const resultsToExitCode = (results: PSLResult): number => {
} else if (results.successes.length) {
return 0;
}
throw new SfError('Invalid results');
throw new SfError('Unexpected state: no successes and no failures. This should not happen.');
};

export const print = (results: PSLResult): void => {
Expand All @@ -97,7 +96,7 @@ export const print = (results: PSLResult): void => {
}

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

Expand All @@ -112,7 +111,7 @@ const usernameToPSLAssignment = async ({
usernameOrAlias: string;
pslId: string;
conn: Connection;
}): Promise<SuccessMsg | FailureMsg> => {
}): Promise<PSLResult> => {
// Convert any aliases to usernames
const resolvedUsername = (await StateAggregator.getInstance()).aliases.resolveUsername(usernameOrAlias);

Expand All @@ -125,38 +124,50 @@ const usernameToPSLAssignment = async ({
AssigneeId,
PermissionSetLicenseId: pslId,
});
return {
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 {
return toResult({
name: resolvedUsername,
value: pslName,
};
});
} else {
return {
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}` }))
);
}
};
13 changes: 7 additions & 6 deletions src/commands/org/assign/permsetlicense.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@
import { Messages } from '@salesforce/core';
import { arrayWithDeprecation, Flags, SfCommand } from '@salesforce/sf-plugins-core';
import { ensureArray } from '@salesforce/kit';
import { assignPSL, print, PSLResult, resultsToExitCode } 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');
Expand Down Expand Up @@ -56,8 +62,3 @@ export class AssignPermSetLicenseCommand extends SfCommand<PSLResult> {
return result;
}
}

const aggregate = (results: PSLResult[]): PSLResult => ({
successes: results.flatMap((r) => r.successes),
failures: results.flatMap((r) => r.failures),
});

0 comments on commit 4ce8080

Please sign in to comment.