Skip to content

Commit

Permalink
Merge pull request #1020 from forcedotcom/cd/record-type-uppercase-co…
Browse files Browse the repository at this point in the history
…nfig

feat(scratch): add `org-capitalize-record-types`
  • Loading branch information
shetzel authored Jan 22, 2024
2 parents 891318a + b0ae322 commit 2f18baa
Show file tree
Hide file tree
Showing 9 changed files with 161 additions and 18 deletions.
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 scratch org creation.

# 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 scratch org creation.
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.
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;

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`;
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'));
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

0 comments on commit 2f18baa

Please sign in to comment.