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

Deploy settings in mdapi format #608

Merged
merged 14 commits into from
Jul 6, 2022
281 changes: 169 additions & 112 deletions src/org/scratchOrgSettingsGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,10 @@

import * as path from 'path';
import { isEmpty, env, upperFirst, Duration } from '@salesforce/kit';
import { ensureObject, getObject, JsonMap, Optional } from '@salesforce/ts-types';
import { ensureObject, getObject, JsonMap } from '@salesforce/ts-types';
import * as js2xmlparser from 'js2xmlparser';
import { Logger } from '../logger';
import { SfError } from '../sfError';
import { JsonAsXml } from '../util/jsonXmlTools';
import { ZipWriter } from '../util/zipWriter';
import { StatusResult } from '../status/types';
import { PollingClient } from '../status/pollingClient';
Expand All @@ -30,23 +29,147 @@ export enum RequestStatus {

const breakPolling = ['Succeeded', 'SucceededPartial', 'Failed', 'Canceled'];

interface ObjectToBusinessProcessPicklist {
[key: string]: {
fullName: string;
default?: boolean;
export interface SettingType {
members: string[];
name: 'CustomObject' | 'RecordType' | 'BusinessProcess' | 'Settings';
}

export interface PackageFile {
'@': {
xmlns: string;
};
types: SettingType[];
version: string;
}

export interface BusinessProcessFileContent extends JsonMap {
fullName: string;
isActive: boolean;
values: [
{
fullName: string;
default?: boolean;
export const createObjectFileContent = ({
allRecordTypes = [],
maggiben marked this conversation as resolved.
Show resolved Hide resolved
allbusinessProcesses = [],
apiVersion,
settingData,
objectSettingsData,
}: {
allRecordTypes?: string[];
allbusinessProcesses?: string[];
apiVersion: string;
settingData?: Record<string, unknown>;
objectSettingsData?: { [objectName: string]: ObjectSetting };
}): PackageFile => {
const output = {
'@': {
xmlns: 'http://soap.sforce.com/2006/04/metadata',
},
types: [] as SettingType[],
};
if (settingData) {
const strings = Object.keys(settingData).map((item) => upperFirst(item).replace('Settings', ''));
output.types.push({ members: strings, name: 'Settings' });
}
if (objectSettingsData) {
const strings = Object.keys(objectSettingsData).map((item) => upperFirst(item));
output.types.push({ members: strings, name: 'CustomObject' });

if (allRecordTypes.length > 0) {
output.types.push({ members: allRecordTypes, name: 'RecordType' });
}
];
}

if (allbusinessProcesses.length > 0) {
output.types.push({ members: allbusinessProcesses, name: 'BusinessProcess' });
}
}
return { ...output, ...{ version: apiVersion } };
};

const calculateBusinessProcess = (objectName: string, defaultRecordType: string) => {
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';
switch (objectName) {
case 'Case':
businessProcessPicklistVal = 'New';
break;
case 'Lead':
businessProcessPicklistVal = 'New - Not Contacted';
break;
case 'Opportunity':
businessProcessPicklistVal = 'Prospecting';
break;
case 'Solution':
businessProcessPicklistVal = 'Draft';
}
}
return [businessProcessName, businessProcessPicklistVal];
};

export const createRecordTypeAndBusinessProcessFileContent = (
objectName: string,
json: Record<string, unknown>,
allRecordTypes: string[],
allbusinessProcesses: string[]
) => {
let output = {
'@': {
xmlns: 'http://soap.sforce.com/2006/04/metadata',
},
} as JsonMap;
const name = upperFirst(objectName);
const sharingModel = json.sharingModel;
if (sharingModel) {
output = {
...output,
sharingModel: upperFirst(sharingModel as string),
};
}

const defaultRecordType = 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);
// Create the record type
const recordTypes = {
fullName: upperFirst(defaultRecordType),
label: upperFirst(defaultRecordType),
active: true,
};

output = {
...output,
recordTypes: {
...recordTypes,
},
};

if (businessProcessName) {
// We need to keep track of these globally for the package.xml
const values: { fullName: string | null; default?: boolean } = {
fullName: businessProcessPicklistVal,
};

if (name !== 'Opportunity') {
values.default = true;
}

allbusinessProcesses.push(`${name}.${businessProcessName}`);
output = {
...output,
recordTypes: {
...recordTypes,
businessProcess: businessProcessName,
},
businessProcesses: {
fullName: businessProcessName,
isActive: true,
values,
},
};
}
}
return output;
};

/**
* Helper class for dealing with the settings that are defined in a scratch definition file. This class knows how to extract the
Expand All @@ -57,7 +180,10 @@ export default class SettingsGenerator {
private objectSettingsData?: { [objectName: string]: ObjectSetting };
private logger: Logger;
private writer: ZipWriter;
private allRecordTypes: string[] = [];
private allbusinessProcesses: string[] = [];
private shapeDirName = `shape_${Date.now()}`;
private packageFilePath = path.join(this.shapeDirName, 'package.xml');

public constructor() {
this.logger = Logger.childFromRoot('SettingsGenerator');
Expand Down Expand Up @@ -89,7 +215,10 @@ export default class SettingsGenerator {
public async createDeploy(): Promise<void> {
const settingsDir = path.join(this.shapeDirName, 'settings');
const objectsDir = path.join(this.shapeDirName, 'objects');
await Promise.all([this.writeSettingsIfNeeded(settingsDir), this.writeObjectSettingsIfNeeded(objectsDir)]);
await Promise.all([
this.writeSettingsIfNeeded(settingsDir),
this.writeObjectSettingsIfNeeded(objectsDir, this.allRecordTypes, this.allbusinessProcesses),
]);
}

/**
Expand All @@ -98,7 +227,15 @@ export default class SettingsGenerator {
public async deploySettingsViaFolder(scratchOrg: Org, apiVersion: string): Promise<void> {
const username = scratchOrg.getUsername();
const logger = await Logger.child('deploySettingsViaFolder');
this.createPackageXml(apiVersion);
const packageObjectProps = createObjectFileContent({
allRecordTypes: this.allRecordTypes,
allbusinessProcesses: this.allbusinessProcesses,
apiVersion,
settingData: this.settingData,
objectSettingsData: this.objectSettingsData,
});
const xml = js2xmlparser.parse('Package', packageObjectProps);
this.writer.addToZip(xml, this.packageFilePath);
await this.writer.finalize();

const connection = scratchOrg.getConnection();
Expand Down Expand Up @@ -161,42 +298,21 @@ export default class SettingsGenerator {
return this.shapeDirName;
}

private async writeObjectSettingsIfNeeded(objectsDir: string) {
if (!this.objectSettingsData || !Object.keys(this.objectSettingsData).length) {
return;
}
for (const objectName of Object.keys(this.objectSettingsData)) {
const value = this.objectSettingsData[objectName];
// writes the object file in source format
const objectDir = path.join(objectsDir, upperFirst(objectName));
const customObjectDir = path.join(objectDir, `${upperFirst(objectName)}.object`);
const customObjectXml = JsonAsXml({
json: this.createObjectFileContent(value),
type: 'RecordType',
});
this.writer.addToZip(customObjectXml, customObjectDir);

if (value.defaultRecordType) {
const recordTypesDir = path.join(objectDir, 'recordTypes', `${upperFirst(value.defaultRecordType)}.recordType`);
const recordTypesFileContent = this.createRecordTypeFileContent(objectName, value);
const recordTypesXml = JsonAsXml({
json: recordTypesFileContent,
type: 'RecordType',
});
this.writer.addToZip(recordTypesXml, recordTypesDir);
// for things that required a businessProcess
if (recordTypesFileContent.businessProcess) {
const businessProcessesDir = path.join(
objectDir,
'businessProcesses',
`${recordTypesFileContent.businessProcess}.businessProcess`
);
const businessProcessesXml = JsonAsXml({
json: this.createBusinessProcessFileContent(objectName, recordTypesFileContent.businessProcess),
type: 'BusinessProcess',
});
this.writer.addToZip(businessProcessesXml, businessProcessesDir);
}
private async writeObjectSettingsIfNeeded(
objectsDir: string,
allRecordTypes: string[],
allbusinessProcesses: string[]
) {
if (this.objectSettingsData) {
for (const [item, value] of Object.entries(this.objectSettingsData)) {
const fileContent = createRecordTypeAndBusinessProcessFileContent(
item,
value,
allRecordTypes,
allbusinessProcesses
);
const xml = js2xmlparser.parse('CustomObject', fileContent);
this.writer.addToZip(xml, path.join(objectsDir, upperFirst(item) + '.object'));
}
}
}
Expand All @@ -212,63 +328,4 @@ export default class SettingsGenerator {
}
}
}

private createPackageXml(apiVersion: string): void {
const pkg = `<?xml version="1.0" encoding="UTF-8"?>
<Package xmlns="http://soap.sforce.com/2006/04/metadata">
<types>
<members>*</members>
<name>Settings</name>
</types>
<types>
<members>*</members>
<name>CustomObject</name>
</types>
<version>${apiVersion}</version>
</Package>`;
this.writer.addToZip(pkg, path.join(this.shapeDirName, 'package.xml'));
}

private createObjectFileContent(json: Record<string, unknown>) {
const output: ObjectSetting = {};
if (json.sharingModel) {
output.sharingModel = upperFirst(json.sharingModel as string);
}
return output;
}

private createRecordTypeFileContent(
objectName: string,
setting: ObjectSetting
): { fullName: Optional<string>; label: Optional<string>; active: boolean; businessProcess?: Optional<string> } {
const defaultRecordType = upperFirst(setting.defaultRecordType);
const output = {
fullName: defaultRecordType,
label: defaultRecordType,
active: true,
};
// all the edge cases
if (['Case', 'Lead', 'Opportunity', 'Solution'].includes(upperFirst(objectName))) {
return { ...output, businessProcess: `${defaultRecordType}Process` };
}
return output;
}

private createBusinessProcessFileContent(
objectName: string,
businessProcessName: string
): BusinessProcessFileContent {
const objectToBusinessProcessPicklist: ObjectToBusinessProcessPicklist = {
Opportunity: { fullName: 'Prospecting' },
Case: { fullName: 'New', default: true },
Lead: { fullName: 'New - Not Contacted', default: true },
Solution: { fullName: 'Draft', default: true },
};

return {
fullName: businessProcessName,
isActive: true,
values: [objectToBusinessProcessPicklist[upperFirst(objectName)]],
};
}
}
Loading