diff --git a/messages/config.md b/messages/config.md index 1fe9b6a29..7039fea6f 100644 --- a/messages/config.md +++ b/messages/config.md @@ -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. diff --git a/messages/envVars.md b/messages/envVars.md index bba841d59..f46ea1c39 100644 --- a/messages/envVars.md +++ b/messages/envVars.md @@ -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. diff --git a/messages/scratchOrgSettingsGenerator.md b/messages/scratchOrgSettingsGenerator.md new file mode 100644 index 000000000..3b2eb5da7 --- /dev/null +++ b/messages/scratchOrgSettingsGenerator.md @@ -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. diff --git a/package.json b/package.json index 3a89bbeb4..fa594e3cc 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/config/envVars.ts b/src/config/envVars.ts index 911675872..a6a2a5bde 100644 --- a/src/config/envVars.ts +++ b/src/config/envVars.ts @@ -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; @@ -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 { diff --git a/src/org/orgConfigProperties.ts b/src/org/orgConfigProperties.ts index 7a6825609..2c2a50f67 100644 --- a/src/org/orgConfigProperties.ts +++ b/src/org/orgConfigProperties.ts @@ -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), diff --git a/src/org/scratchOrgCreate.ts b/src/org/scratchOrgCreate.ts index ce2cc3e64..414a8fcc9 100644 --- a/src/org/scratchOrgCreate.ts +++ b/src/org/scratchOrgCreate.ts @@ -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'; @@ -150,7 +150,12 @@ export const scratchOrgResume = async (jobId: string): Promise => { // a project isn't required for org:create } }; + +async function getCapitalizeRecordTypesConfig(): Promise { + 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; +} diff --git a/src/org/scratchOrgSettingsGenerator.ts b/src/org/scratchOrgSettingsGenerator.ts index 575bf0c42..1d9d62d19 100644 --- a/src/org/scratchOrgSettingsGenerator.ts +++ b/src/org/scratchOrgSettingsGenerator.ts @@ -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', @@ -82,13 +86,19 @@ export const createObjectFileContent = ({ return { ...output, ...{ version: apiVersion } }; }; -const calculateBusinessProcess = (objectName: string, defaultRecordType: string): Array => { +const calculateBusinessProcess = ( + objectName: string, + defaultRecordType: string, + capitalizeBusinessProcess: boolean +): Array => { 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'; @@ -110,7 +120,8 @@ export const createRecordTypeAndBusinessProcessFileContent = ( objectName: string, json: Record, allRecordTypes: string[], - allBusinessProcesses: string[] + allBusinessProcesses: string[], + capitalizeRecordTypes: boolean ): JsonMap => { let output = { '@': { @@ -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, }; @@ -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()}`; @@ -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')); diff --git a/test/unit/org/scratchOrgSettingsGeneratorTest.ts b/test/unit/org/scratchOrgSettingsGeneratorTest.ts index 9d137f508..b7b1088d1 100644 --- a/test/unit/org/scratchOrgSettingsGeneratorTest.ts +++ b/test/unit/org/scratchOrgSettingsGeneratorTest.ts @@ -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' }, @@ -649,6 +650,30 @@ 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[] = []; @@ -656,7 +681,8 @@ describe('scratchOrgSettingsGenerator', () => { 'opportunity', objectSettingsData.opportunity, allRecordTypes, - allbusinessProcesses + allbusinessProcesses, + true ); expect(recordTypeAndBusinessProcessFileContent).to.deep.equal({ '@': { xmlns: 'http://soap.sforce.com/2006/04/metadata' }, @@ -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' }, @@ -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', () => {