Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(scratch): add org-capitalize-record-types #1020

Merged
merged 6 commits into from
Jan 22, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions messages/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,10 @@ A valid repository URL or directory for the custom org metadata templates.

A valid repository URL or directory for the custom org metadata templates.

# org-capitalize-record-types

Whether record types are capitalized on scrach org creation.
cristiand391 marked this conversation as resolved.
Show resolved Hide resolved

# invalidId

The given id %s is not a valid 15 or 18 character Salesforce ID.
4 changes: 4 additions & 0 deletions messages/envVars.md
Original file line number Diff line number Diff line change
Expand Up @@ -311,3 +311,7 @@ Deprecated environment variable: %s. Please use %s instead.
Deprecated environment variable: %s. Please use %s instead.

Your environment has both variables populated, and with different values. The value from %s will be used.

# sfCapitalizeRecordTypes

Whether record types are capitalized on scrach org creation.
cristiand391 marked this conversation as resolved.
Show resolved Hide resolved
4 changes: 4 additions & 0 deletions messages/scratchOrgSettingsGenerator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# noCapitalizeRecordTypeConfigVar

Record types defined in the scratch org definition file will stop being capitalized by default in a future release.
Set the `org-capitalize-record-types` config var to `true` to enforce capitalization.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this warning will be visible to both projects using sfdx-core and the CLI.

For lib users, I wanted to make this an object option rather than the config (that way the config var would live in plugin-org) but decided against it (more about that below).

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@salesforce/core",
"version": "6.4.7",
"version": "6.4.8-dev.0",
"description": "Core libraries to interact with SFDX projects, orgs, and APIs.",
"main": "lib/exported",
"types": "lib/exported.d.ts",
Expand Down
5 changes: 5 additions & 0 deletions src/config/envVars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export enum EnvironmentVariable {
'SF_UPDATE_INSTRUCTIONS' = 'SF_UPDATE_INSTRUCTIONS',
'SF_INSTALLER' = 'SF_INSTALLER',
'SF_ENV' = 'SF_ENV',
'SF_CAPITALIZE_RECORD_TYPES' = 'SF_CAPITALIZE_RECORD_TYPES',
}
type EnvMetaData = {
description: string;
Expand Down Expand Up @@ -417,6 +418,10 @@ export const SUPPORTED_ENV_VARS: EnvType = {
description: getMessage(EnvironmentVariable.SF_ENV),
synonymOf: null,
},
[EnvironmentVariable.SF_CAPITALIZE_RECORD_TYPES]: {
description: getMessage(EnvironmentVariable.SF_CAPITALIZE_RECORD_TYPES),
synonymOf: null,
},
};

export class EnvVars extends Env {
Expand Down
8 changes: 8 additions & 0 deletions src/org/orgConfigProperties.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,17 @@ export enum OrgConfigProperties {
* The url for the debugger configuration.
*/
ORG_ISV_DEBUGGER_URL = 'org-isv-debugger-url',
/**
* Capitalize record types when deploying scratch org settings
*/
ORG_CAPITALIZE_RECORD_TYPES = 'org-capitalize-record-types',
}

export const ORG_CONFIG_ALLOWED_PROPERTIES = [
{
key: OrgConfigProperties.ORG_CAPITALIZE_RECORD_TYPES,
description: messages.getMessage(OrgConfigProperties.ORG_CAPITALIZE_RECORD_TYPES),
},
{
key: OrgConfigProperties.ORG_CUSTOM_METADATA_TEMPLATES,
description: messages.getMessage(OrgConfigProperties.ORG_CUSTOM_METADATA_TEMPLATES),
Expand Down
26 changes: 23 additions & 3 deletions src/org/scratchOrgCreate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +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 { Duration } from '@salesforce/kit';
import { Duration, toBoolean } from '@salesforce/kit';
import { ensureString } from '@salesforce/ts-types';
import { Messages } from '../messages';
import { Logger } from '../logger/logger';
Expand Down Expand Up @@ -150,7 +150,12 @@ export const scratchOrgResume = async (jobId: string): Promise<ScratchOrgCreateR
const configAggregator = await ConfigAggregator.create();

await emit({ stage: 'deploy settings', scratchOrgInfo: soi });
const settingsGenerator = new SettingsGenerator();

const capitalizeRecordTypes = await getCapitalizeRecordTypesConfig();

const settingsGenerator = new SettingsGenerator({
capitalizeRecordTypes,
});
await settingsGenerator.extract({ ...soi, ...definitionjson });
const [authInfo] = await Promise.all([
resolveUrl(scratchOrgAuthInfo),
Expand Down Expand Up @@ -228,8 +233,13 @@ export const scratchOrgCreate = async (options: ScratchOrgCreateOptions): Promis
ignoreAncestorIds,
});

const capitalizeRecordTypes = await getCapitalizeRecordTypesConfig();

// gets the scratch org settings (will use in both signup paths AND to deploy the settings)
const settingsGenerator = new SettingsGenerator();
const settingsGenerator = new SettingsGenerator({
capitalizeRecordTypes,
});

const settings = await settingsGenerator.extract(scratchOrgInfo);
logger.debug(`the scratch org def file has settings: ${settingsGenerator.hasSettings()}`);

Expand Down Expand Up @@ -328,3 +338,13 @@ const getSignupTargetLoginUrl = async (): Promise<string | undefined> => {
// a project isn't required for org:create
}
};

async function getCapitalizeRecordTypesConfig(): Promise<boolean | undefined> {
const configAgg = await ConfigAggregator.create();
const value = configAgg.getInfo('org-capitalize-record-types').value as string | undefined;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the type of value is AnyJson | undefined, at runtime we have 2 possible options:

  1. a string containing true or false. NOTE this is always a string, not a boolean
    2 undefined

if it's undefined then we want to return that so SettingsGenerator knows there's no config var and emits the warning.

if not undefined we cast it to boolean and return it.


if (value !== undefined) return toBoolean(value);

// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
return value as undefined;
}
55 changes: 44 additions & 11 deletions src/org/scratchOrgSettingsGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,13 @@ import { StatusResult } from '../status/types';
import { PollingClient } from '../status/pollingClient';
import { ZipWriter } from '../util/zipWriter';
import { DirectoryWriter } from '../util/directoryWriter';
import { Lifecycle } from '../lifecycleEvents';
import { Messages } from '../messages';
import { ScratchOrgInfo, ObjectSetting } from './scratchOrgTypes';
import { Org } from './org';

Messages.importMessagesDirectory(__dirname);

export enum RequestStatus {
Pending = 'Pending',
InProgress = 'InProgress',
Expand Down Expand Up @@ -82,13 +86,19 @@ export const createObjectFileContent = ({
return { ...output, ...{ version: apiVersion } };
};

const calculateBusinessProcess = (objectName: string, defaultRecordType: string): Array<string | null> => {
const calculateBusinessProcess = (
objectName: string,
defaultRecordType: string,
capitalizeBusinessProcess: boolean
): Array<string | null> => {
let businessProcessName = null;
let businessProcessPicklistVal = null;
// These four objects require any record type to specify a "business process"--
// a restricted set of items from a standard picklist on the object.
if (['Case', 'Lead', 'Opportunity', 'Solution'].includes(objectName)) {
businessProcessName = upperFirst(defaultRecordType) + 'Process';
businessProcessName = capitalizeBusinessProcess
? `${upperFirst(defaultRecordType)}Process`
: `${defaultRecordType}Process`;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this wasn't in the scope of the inv. we got but it's the same behavior (for the listed 4 objects above) so I made this follow the config var too.

switch (objectName) {
case 'Case':
businessProcessPicklistVal = 'New';
Expand All @@ -110,7 +120,8 @@ export const createRecordTypeAndBusinessProcessFileContent = (
objectName: string,
json: Record<string, unknown>,
allRecordTypes: string[],
allBusinessProcesses: string[]
allBusinessProcesses: string[],
capitalizeRecordTypes: boolean
): JsonMap => {
let output = {
'@': {
Expand All @@ -126,15 +137,23 @@ export const createRecordTypeAndBusinessProcessFileContent = (
};
}

const defaultRecordType = json.defaultRecordType;
const defaultRecordType = capitalizeRecordTypes
? upperFirst(json.defaultRecordType as string)
: json.defaultRecordType;

if (typeof defaultRecordType === 'string') {
// We need to keep track of these globally for when we generate the package XML.
allRecordTypes.push(`${name}.${upperFirst(defaultRecordType)}`);
const [businessProcessName, businessProcessPicklistVal] = calculateBusinessProcess(name, defaultRecordType);
allRecordTypes.push(`${name}.${defaultRecordType}`);
const [businessProcessName, businessProcessPicklistVal] = calculateBusinessProcess(
name,
defaultRecordType,
capitalizeRecordTypes
);

// Create the record type
const recordTypes = {
fullName: upperFirst(defaultRecordType),
label: upperFirst(defaultRecordType),
fullName: defaultRecordType,
label: defaultRecordType,
active: true,
};

Expand Down Expand Up @@ -186,9 +205,22 @@ export default class SettingsGenerator {
private allBusinessProcesses: string[] = [];
private readonly shapeDirName: string;
private readonly packageFilePath: string;

public constructor(options?: { mdApiTmpDir?: string; shapeDirName?: string; asDirectory?: boolean }) {
private readonly capitalizeRecordTypes: boolean;

public constructor(options?: {
mdApiTmpDir?: string;
shapeDirName?: string;
asDirectory?: boolean;
capitalizeRecordTypes?: boolean;
}) {
this.logger = Logger.childFromRoot('SettingsGenerator');
if (options?.capitalizeRecordTypes === undefined) {
const messages = Messages.loadMessages('@salesforce/core', 'scratchOrgSettingsGenerator');
void Lifecycle.getInstance().emitWarning(messages.getMessage('noCapitalizeRecordTypeConfigVar'));
shetzel marked this conversation as resolved.
Show resolved Hide resolved
this.capitalizeRecordTypes = true;
} else {
this.capitalizeRecordTypes = options.capitalizeRecordTypes;
}
// If SFDX_MDAPI_TEMP_DIR is set, copy settings to that dir for people to inspect.
const mdApiTmpDir = options?.mdApiTmpDir ?? env.getString('SFDX_MDAPI_TEMP_DIR');
this.shapeDirName = options?.shapeDirName ?? `shape_${Date.now()}`;
Expand Down Expand Up @@ -344,7 +376,8 @@ export default class SettingsGenerator {
item,
value,
allRecordTypes,
allbusinessProcesses
allbusinessProcesses,
this.capitalizeRecordTypes
);
const xml = js2xmlparser.parse('CustomObject', fileContent);
return this.writer.addToStore(xml, path.join(objectsDir, upperFirst(item) + '.object'));
Expand Down
71 changes: 68 additions & 3 deletions test/unit/org/scratchOrgSettingsGeneratorTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -639,7 +639,8 @@ describe('scratchOrgSettingsGenerator', () => {
'account',
objectSettingsData.account,
allRecordTypes,
allbusinessProcesses
allbusinessProcesses,
true
);
expect(recordTypeAndBusinessProcessFileContent).to.deep.equal({
'@': { xmlns: 'http://soap.sforce.com/2006/04/metadata' },
Expand All @@ -649,14 +650,39 @@ describe('scratchOrgSettingsGenerator', () => {
expect(allbusinessProcesses).to.deep.equal([]);
});

it('createRecordTypeAndBusinessProcessFileContent with account type, not capitalized', () => {
const objectSettingsDataLowercaseRecordType = {
account: {
defaultRecordType: 'personAccount',
},
};

const allRecordTypes: string[] = [];
const allbusinessProcesses: string[] = [];
const recordTypeAndBusinessProcessFileContent = createRecordTypeAndBusinessProcessFileContent(
'account',
objectSettingsDataLowercaseRecordType.account,
allRecordTypes,
allbusinessProcesses,
false
);
expect(recordTypeAndBusinessProcessFileContent).to.deep.equal({
'@': { xmlns: 'http://soap.sforce.com/2006/04/metadata' },
recordTypes: { fullName: 'personAccount', label: 'personAccount', active: true },
});
expect(allRecordTypes).to.deep.equal(['Account.personAccount']);
expect(allbusinessProcesses).to.deep.equal([]);
});

it('createRecordTypeAndBusinessProcessFileContent with opportunity values', () => {
const allRecordTypes: string[] = [];
const allbusinessProcesses: string[] = [];
const recordTypeAndBusinessProcessFileContent = createRecordTypeAndBusinessProcessFileContent(
'opportunity',
objectSettingsData.opportunity,
allRecordTypes,
allbusinessProcesses
allbusinessProcesses,
true
);
expect(recordTypeAndBusinessProcessFileContent).to.deep.equal({
'@': { xmlns: 'http://soap.sforce.com/2006/04/metadata' },
Expand Down Expand Up @@ -686,7 +712,8 @@ describe('scratchOrgSettingsGenerator', () => {
'case',
objectSettingsData.case,
allRecordTypes,
allbusinessProcesses
allbusinessProcesses,
true
);
expect(recordTypeAndBusinessProcessFileContent).to.deep.equal({
'@': { xmlns: 'http://soap.sforce.com/2006/04/metadata' },
Expand All @@ -709,6 +736,44 @@ describe('scratchOrgSettingsGenerator', () => {
expect(allRecordTypes).to.deep.equal(['Case.Default']);
expect(allbusinessProcesses).to.deep.equal(['Case.DefaultProcess']);
});

it('createRecordTypeAndBusinessProcessFileContent with case values, not capitalized', () => {
const objectSettingsDataLowercaseRecordType = {
case: {
defaultRecordType: 'default',
sharingModel: 'private',
},
};
const allRecordTypes: string[] = [];
const allbusinessProcesses: string[] = [];
const recordTypeAndBusinessProcessFileContent = createRecordTypeAndBusinessProcessFileContent(
'case',
objectSettingsDataLowercaseRecordType.case,
allRecordTypes,
allbusinessProcesses,
false
);
expect(recordTypeAndBusinessProcessFileContent).to.deep.equal({
'@': { xmlns: 'http://soap.sforce.com/2006/04/metadata' },
sharingModel: 'Private',
recordTypes: {
fullName: 'default',
label: 'default',
active: true,
businessProcess: 'defaultProcess',
},
businessProcesses: {
fullName: 'defaultProcess',
isActive: true,
values: {
fullName: 'New',
default: true,
},
},
});
expect(allRecordTypes).to.deep.equal(['Case.default']);
expect(allbusinessProcesses).to.deep.equal(['Case.defaultProcess']);
});
});

describe('createObjectFileContent', () => {
Expand Down