Skip to content

Commit

Permalink
fix: no nullish in template literals (#868)
Browse files Browse the repository at this point in the history
* fix: no nullish in template literals

* test: can run non-local nuts from script

* fix: nut failure (order of validations for describe

* fix: truthy 0 mistake (thanks, NUTs!)

* test: oh, the UT doesn't match the type.  allow string or number
  • Loading branch information
mshanemc authored Jul 19, 2024
1 parent 9497bb5 commit ea0d3b9
Show file tree
Hide file tree
Showing 7 changed files with 134 additions and 421 deletions.
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,14 @@
],
"bugs": "https://github.com/salesforcecli/plugin-custom-metadata/issues",
"dependencies": {
"@oclif/core": "^4",
"@salesforce/core": "^8.1.0",
"@salesforce/sf-plugins-core": "^11.1.7",
"csv-parse": "^5.5.6",
"fast-xml-parser": "^4.4.0"
},
"devDependencies": {
"@jsforce/jsforce-node": "^3.2.0",
"@jsforce/jsforce-node": "^3.2.2",
"@oclif/core": "^4.0.7",
"@oclif/plugin-command-snapshot": "^5.2.5",
"@salesforce/cli-plugins-testkit": "^5.3.18",
"@salesforce/dev-scripts": "^10.2.2",
Expand Down Expand Up @@ -117,6 +117,7 @@
"test": "wireit",
"test:nuts": "nyc mocha \"**/*.nut.ts\" --slow 4500 --timeout 600000 --parallel --jobs 20",
"test:nuts:local": "nyc mocha \"**/local/*.nut.ts\" --slow 4500 --timeout 600000 --parallel --jobs 20",
"test:nuts:remote": "nyc mocha \"**/nuts/*.nut.ts\" --slow 4500 --timeout 600000 --parallel --jobs 20",
"test:only": "wireit",
"version": "oclif readme"
},
Expand Down
62 changes: 44 additions & 18 deletions src/commands/cmdt/generate/fromorg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ import {
isValidMetadataRecordName,
} from '../../../shared/helpers/validationUtil.js';
import { canConvert, createObjectXML, createFieldXML } from '../../../shared/templates/templates.js';
import {
ensureFullName,
CustomFieldWithFullNameTypeAndLabel,
CustomObjectWithFullName,
} from '../../../shared/types.js';

Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
const messages = Messages.loadMessages('@salesforce/plugin-custom-metadata', 'fromorg');
Expand All @@ -33,6 +38,7 @@ export type CmdtGenerateResponse = {
outputDir: string;
recordsOutputDir: string;
};

export default class Generate extends SfCommand<CmdtGenerateResponse> {
public static readonly summary = messages.getMessage('summary');
public static readonly description = messages.getMessage('description');
Expand Down Expand Up @@ -101,21 +107,10 @@ export default class Generate extends SfCommand<CmdtGenerateResponse> {
const conn = flags['target-org'].getConnection(flags['api-version']);

// use default target org connection to get object describe if no source is provided.
const describeObj = await conn.metadata.read('CustomObject', flags.sobject);

// throw error if the object doesnot exist(empty json as response from the describe call.)
if (describeObj.fields.length === 0) {
const errMsg = messages.getMessage('sobjectnameNoResultError', [flags.sobject]);
throw new SfError(errMsg, 'sobjectnameNoResultError');
}
// check for custom setting
if (describeObj.customSettingsType) {
// if custom setting check for type and visibility
if (!validCustomSettingType(describeObj)) {
const errMsg = messages.getMessage('customSettingTypeError', [flags.sobject]);
throw new SfError(errMsg, 'customSettingTypeError');
}
}
const describeObj = validateCustomObjectDescribe(
await conn.metadata.read('CustomObject', flags.sobject),
flags.sobject
);

const label = flags.label ?? flags['dev-name'];
const pluralLabel = flags['plural-label'] ?? label;
Expand All @@ -132,11 +127,16 @@ export default class Generate extends SfCommand<CmdtGenerateResponse> {
// get all the field details before creating field metadata
const fields = describeObjFields(describeObj)
// added type check here to skip the creation of un supported fields
.filter(fieldHasFullnameTypeLabel)
.filter((f) => !flags['ignore-unsupported'] || canConvert(f['type']))
.flatMap((f) =>
// check for Geo Location fields before hand and create two different fields for longitude and latitude.
f.type !== 'Location' ? [f] : convertLocationFieldToText(f)
);
/* if there's no fullName, we won't be able to write the file.
* in the wsdl, metadata types inherit fullName (optional) from the Metadata base type,
* but CustomObject does always have one */

// create custom metdata fields
await Promise.all(
fields.map((f) =>
Expand Down Expand Up @@ -202,13 +202,21 @@ export default class Generate extends SfCommand<CmdtGenerateResponse> {
}
}

const getSoqlQuery = (describeResult: CustomObject): string => {
const fieldNames = describeResult.fields.map((field) => field.fullName).join(',');
const fieldHasFullnameTypeLabel = (field: CustomField): field is CustomFieldWithFullNameTypeAndLabel =>
typeof field.fullName === 'string' && typeof field.label === 'string' && typeof field.type === 'string';

const getSoqlQuery = (describeResult: CustomObjectWithFullName): string => {
const fieldNames = describeResult.fields
.filter(fieldHasFullnameTypeLabel)
.map((field) => field.fullName)
.join(',');
// Added Name hardcoded as Name field is not retrieved as part of object describe.
return `SELECT Name, ${fieldNames} FROM ${describeResult.fullName}`;
};

const convertLocationFieldToText = (field: CustomField): CustomField[] => {
const convertLocationFieldToText = (
field: CustomFieldWithFullNameTypeAndLabel
): CustomFieldWithFullNameTypeAndLabel[] => {
const baseTextField = {
required: field['required'],
trackHistory: field['trackHistory'],
Expand All @@ -223,3 +231,21 @@ const convertLocationFieldToText = (field: CustomField): CustomField[] => {
label: `${prefix}${field.label}`,
}));
};

/** throw errors if describe is empty (doesn't exist) or is CustomSetting or missing fullname */
const validateCustomObjectDescribe = (describeObj: CustomObject, objectName: string): CustomObjectWithFullName => {
// throw error if the object doesnot exist(empty json as response from the describe call.)
if (describeObj.fields.length === 0) {
const errMsg = messages.getMessage('sobjectnameNoResultError', [objectName]);
throw new SfError(errMsg, 'sobjectnameNoResultError');
}
// check for custom setting
if (describeObj.customSettingsType) {
// if custom setting check for type and visibility
if (!validCustomSettingType(describeObj)) {
const errMsg = messages.getMessage('customSettingTypeError', [objectName]);
throw new SfError(errMsg, 'customSettingTypeError');
}
}
return ensureFullName(describeObj);
};
1 change: 0 additions & 1 deletion src/shared/helpers/createUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,6 @@ export const createRecord = async (createConfig: CreateConfig): Promise<void> =>
*/
export const getFieldPrimitiveType = (fileData: CustomField[] = [], fieldName?: string): string => {
const matchingFile = fileData.find((file) => file.fullName === fieldName);

if (matchingFile && typeof matchingFile.type === 'string' && ['Number', 'Percent'].includes(matchingFile.type)) {
return getNumberType(matchingFile.type, matchingFile.scale);
}
Expand Down
11 changes: 4 additions & 7 deletions src/shared/helpers/fileWriter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ type FileWriterResult = {
dir: string;
fileName: string;
updated: boolean;
}
};

/**
* Using the given file system, creates a file representing a new custom metadata type.
Expand Down Expand Up @@ -60,15 +60,11 @@ export const writeTypeFile = async (
export const writeFieldFile = async (
corefs = fs,
dir: string,
fieldName: string | undefined | null,
fieldName: string,
fieldXML: string
): Promise<FileWriterResult> => {
// appending __c if its not already there
if (fieldName?.endsWith('__c') === false) {
fieldName += '__c';
}
const outputFilePath = path.join(removeTrailingSlash(dir), 'fields');
const fileName = `${fieldName}.field-meta.xml`;
const fileName = `${ensureDoubleUnderscoreC(fieldName)}.field-meta.xml`;
const updated = fs.existsSync(path.join(outputFilePath, fileName));
await corefs.promises.mkdir(outputFilePath, { recursive: true });
await corefs.promises.writeFile(path.join(outputFilePath, fileName), fieldXML);
Expand All @@ -77,3 +73,4 @@ export const writeFieldFile = async (
};

const removeTrailingSlash = (dir: string): string => dir.replace(/\/+$/, '');
const ensureDoubleUnderscoreC = (name: string): string => (name.endsWith('__c') ? name : `${name}__c`);
19 changes: 11 additions & 8 deletions src/shared/templates/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import { SfError, Messages } from '@salesforce/core';
import type { CustomValue, CustomField } from '@jsforce/jsforce-node/lib/api/metadata.js';
import { CustomFieldWithFullNameTypeAndLabel, customValueHasFullNameAndLabel } from '../types.js';

Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
const messages = Messages.loadMessages('@salesforce/plugin-custom-metadata', 'template');
Expand Down Expand Up @@ -38,7 +39,7 @@ export const createObjectXML = (
* @param data Record details
* @param defaultToString If the defaultToString set type to Text for unsupported field types
*/
export const createFieldXML = (data: CustomField, defaultToString: boolean): string => {
export const createFieldXML = (data: CustomFieldWithFullNameTypeAndLabel, defaultToString: boolean): string => {
let returnValue = '<?xml version="1.0" encoding="UTF-8"?>\n';
returnValue += '<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">\n';
returnValue += getFullName(data);
Expand Down Expand Up @@ -72,7 +73,7 @@ export const createDefaultTypeStructure = (
label: string,
picklistValues: string[] = [],
decimalplaces = 0
): CustomField => {
): CustomFieldWithFullNameTypeAndLabel => {
const precision = 18 - decimalplaces;
const scale = decimalplaces;
const baseObject = { fullName, type, label, summaryFilterItems: [] };
Expand Down Expand Up @@ -133,7 +134,7 @@ export const canConvert = (type: string | undefined | null): boolean => {
const createPicklistValues = (values: string[]): CustomValue[] =>
values.map((value) => ({ fullName: value, label: value, default: false }));

const getType = (data: CustomField, defaultToMetadataType: boolean): string => {
const getType = (data: CustomFieldWithFullNameTypeAndLabel, defaultToMetadataType: boolean): string => {
if (canConvert(data.type)) {
// To handle the text formula field scenario where field type will be Text with no length attribute
if (data.type === 'Text' && data.length === undefined) {
Expand Down Expand Up @@ -179,14 +180,14 @@ const getDefaultValue = (data: CustomField): string => {
return data.defaultValue ? `\t<defaultValue>${data.defaultValue}</defaultValue>\n` : '';
};

const getValueSet = (data: CustomField): string => {
const getValueSet = (data: CustomFieldWithFullNameTypeAndLabel): string => {
let fieldValue = '';
if (data.valueSet) {
fieldValue += '\t<valueSet>\n';
fieldValue += `\t\t<restricted>${data.valueSet.restricted ?? false}</restricted>\n`;
fieldValue += '\t\t<valueSetDefinition>\n';
fieldValue += `\t\t\t<sorted>${data.valueSet.valueSetDefinition?.sorted ?? false}</sorted>\n`;
data.valueSet.valueSetDefinition?.value.forEach((value) => {
(data.valueSet.valueSetDefinition?.value ?? []).filter(customValueHasFullNameAndLabel).map((value) => {
fieldValue += '\t\t\t<value>\n';
fieldValue += `\t\t\t\t<fullName>${value.fullName}</fullName>\n`;
fieldValue += `\t\t\t\t<default>${value.default || false}</default>\n`;
Expand All @@ -211,7 +212,7 @@ const getConvertType = (data: CustomField): string => {
}
};

const getFullName = (data: CustomField): string => {
const getFullName = (data: CustomFieldWithFullNameTypeAndLabel): string => {
const name = data.fullName?.endsWith('__c') ? data.fullName : `${data.fullName}__c`;
return `\t<fullName>${name}</fullName>\n`;
};
Expand All @@ -228,7 +229,7 @@ const getFieldManageability = (data: CustomField): string =>
const getInlineHelpText = (data: CustomField): string =>
data.inlineHelpText ? `\t<inlineHelpText>${data.inlineHelpText}</inlineHelpText>\n` : '';

const getLabel = (data: CustomField): string => `\t<label>${data.label}</label>\n`;
const getLabel = (data: CustomFieldWithFullNameTypeAndLabel): string => `\t<label>${data.label}</label>\n`;

const getRequiredTag = (data: CustomField): string =>
typeof data.unique === 'boolean' ? `\t<unique>${data.unique}</unique>\n` : '';
Expand All @@ -237,4 +238,6 @@ const getPrecisionTag = (data: CustomField): string =>
data.precision ? `\t<precision>${data.precision}</precision>\n` : '';

const getScaleTag = (data: CustomField): string =>
typeof data.scale !== 'undefined' ? `\t<scale>${data.scale}</scale>\n` : '';
// CustomField thinks this is a number. The UT had it as a string.
// This will work for either(filtering out null / undefined because only ==)
typeof data.scale !== null ? `\t<scale>${data.scale}</scale>\n` : '';
33 changes: 33 additions & 0 deletions src/shared/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright (c) 2023, salesforce.com, inc.
* All rights reserved.
* 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 type { CustomField, CustomObject, CustomValue } from '@jsforce/jsforce-node/lib/api/metadata.js';

// I know, right? The jsforce types aren't the best. We expect some properties to be present that it doesn't guarantee.
// All this might improve when jsforce starts using @salesforce/schemas for its md types (and then we can drop the dependency on jsforce-node here)

export type CustomFieldWithFullNameTypeAndLabel = CustomField &
NonNullableFields<Pick<Required<CustomField>, 'fullName' | 'type' | 'label'>>;

export type CustomObjectWithFullName = CustomObject & { fullName: string };

export type CustomValueWithFullNameAndLabel = CustomValue &
Required<NonNullableFields<Pick<CustomValue, 'fullName' | 'label'>>>;

export const ensureFullName = (obj: CustomObject): CustomObjectWithFullName => {
if (typeof obj.fullName === 'string') {
return obj as CustomObjectWithFullName;
}
throw new Error('CustomObject must have a fullName');
};

export const customValueHasFullNameAndLabel = (cv: CustomValue): cv is CustomValueWithFullNameAndLabel =>
typeof cv.fullName === 'string' && typeof cv.label === 'string';

/** make all fields non-nullable and required */
type NonNullableFields<T> = {
[P in keyof T]: NonNullable<T[P]>;
};
Loading

0 comments on commit ea0d3b9

Please sign in to comment.