Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
41 changes: 41 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,46 @@
# Change Log

## [2.7.0] [PR#35](https://github.com/jdschleicher/Salesforce-Data-Treecipe/pull/35) - Feature: Enhanced Text & Numeric Field Precision Handling

### 🎯 Major Features

#### 1. **Text and Numeric Value Constraints**
Added new methods for building text and numeric recipe values with constraints, enhancing control over generated data ranges and formats.

### 🔧 Technical Details

**Code Example - Currency Field XML to FakerJS YAML**:

Given a custom object field XML markup with precision and scale:

```xml
<fields>
<fullName>Price__c</fullName>
<description>Product price</description>
<externalId>false</externalId>
<label>Price</label>
<precision>8</precision>
<required>false</required>
<scale>2</scale>
<type>Currency</type>
<unique>false</unique>
</fields>
```

The generated recipe automatically creates a faker expression that respects the precision (8) and scale (2):

```yaml
- object: My_Custom_Object__c
fields:
Price__c: ${{ faker.finance.amount({ min: 0, max: 999999, dec: 2 }) }}
```

This ensures the generated currency values:
- Have at most 8 total digits (precision)
- Have exactly 2 decimal places (scale)
- Are within the valid range (0 to 99,999.99)
- Match Salesforce field constraints

## [2.6.0] [PR#34](https://github.com/jdschleicher/Salesforce-Data-Treecipe/pull/34) - Feature: Relationship Service & Bug Fix for Special Characters in Picklists

### 🎯 Major Features
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"displayName": "Salesforce Data Treecipe",
"description": "source-fidelity driven development, pairs well with cumulus-ci",
"icon": "images/datatreecipe.webp",
"version": "2.6.0",
"version": "2.7.0",
"engines": {
"vscode": "^1.94.0"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -504,7 +504,37 @@ ${this.generateTabs(5)}${randomChoicesBreakdown}`;
getMultipicklistTODOPlaceholderWithExample():string {

const emptyMultiSelectXMLDetailPlaceholder = `### TODO: POSSIBLE GLOBAL OR STANDARD VALUE SET USED FOR THIS MULTIPICKLIST AS DETAILS ARE NOT IN FIELD XML MARKUP -- FIND ASSOCIATED VALUE SET AND REPLACE COMMA SEPARATED FRUITS WITH VALUE SET OPTIONS: \${{ (faker.helpers.arrayElements(['apple', 'orange', 'banana']) ).join(';') }}`;
return emptyMultiSelectXMLDetailPlaceholder;
return emptyMultiSelectXMLDetailPlaceholder;

}

buildTextRecipeValueWithLength(length: number): string {
return `${this.openingRecipeSyntax} faker.lorem.text(${length}).substring(0, ${length}) ${this.closingRecipeSyntax}`;
}

buildNumericRecipeValueWithPrecisionAndScale(precision: number, scale?: number): string {

const effectiveScale = scale ?? 0;
const maxNumbersLeftOfDecimal = '9'.repeat(precision - effectiveScale);

if (effectiveScale === 0) {
return `|
${this.openingRecipeSyntax} faker.number.int({min: 0, max: ${maxNumbersLeftOfDecimal}}) ${this.closingRecipeSyntax}`;
} else {
return `|
${this.openingRecipeSyntax} faker.finance.amount({min: 0, max: ${maxNumbersLeftOfDecimal}, dec: ${effectiveScale}}) ${this.closingRecipeSyntax}`;
}

}

buildCurrencyRecipeValueWithPrecisionAndScale(precision: number, scale?: number): string {
// Special handling for currency fields - use full precision as left_digits

const effectiveScale = scale ?? 0;
const maxNumbersLeftOfDecimal = '9'.repeat(precision - effectiveScale);

return `|
${this.openingRecipeSyntax} faker.finance.amount({min: 0, max: ${maxNumbersLeftOfDecimal}, dec: ${effectiveScale}}) ${this.closingRecipeSyntax}`;

}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,7 @@ export interface IRecipeFakerService {
controllingValue: string): string
getMultipicklistTODOPlaceholderWithExample(): string
getStandardAndGlobalValueSetTODOPlaceholderWithExample(): string
}
buildTextRecipeValueWithLength(length: number): string
buildNumericRecipeValueWithPrecisionAndScale(precision: number, scale?: number): string
buildCurrencyRecipeValueWithPrecisionAndScale(precision: number, scale?: number): string
}
Original file line number Diff line number Diff line change
Expand Up @@ -397,7 +397,32 @@ ${this.generateTabs(5)}${randomChoicesBreakdown}`;

const emptyMultiSelectXMLDetailPlaceholder = `### TODO: POSSIBLE GLOBAL OR STANDARD VALUE SET USED FOR THIS MULTIPICKLIST AS DETAILS ARE NOT IN FIELD XML MARKUP -- FIND ASSOCIATED VALUE SET AND REPLACE COMMA SEPARATED FRUITS WITH VALUE SET OPTIONS: \${{ (';').join((fake.random_sample(elements=('apple', 'orange', 'banana')))) }}`;
return emptyMultiSelectXMLDetailPlaceholder;

}

buildTextRecipeValueWithLength(length: number): string {
return `${this.openingRecipeSyntax}fake.text(max_nb_chars=${length})${this.closingRecipeSyntax}`;
}

buildNumericRecipeValueWithPrecisionAndScale(precision: number, scale?: number): string {

const effectiveScale = scale ?? 0;
const maxNumbersLeftOfDecimal = '9'.repeat(precision - effectiveScale);

if (effectiveScale === 0) {
return `${this.openingRecipeSyntax}fake.random_int(min=0, max=${maxNumbersLeftOfDecimal})${this.closingRecipeSyntax}`;
} else {
return `${this.openingRecipeSyntax}fake.pydecimal(left_digits=${maxNumbersLeftOfDecimal}, right_digits=${effectiveScale}, positive=True)${this.closingRecipeSyntax}`;
}

}

buildCurrencyRecipeValueWithPrecisionAndScale(precision: number, scale?: number): string {

const effectiveScale = scale ?? 0;
const maxNumbersLeftOfDecimal = precision - effectiveScale;
return `${this.openingRecipeSyntax}fake.pydecimal(left_digits=${maxNumbersLeftOfDecimal}, right_digits=${effectiveScale}, positive=True)${this.closingRecipeSyntax}`;

}

}
}
52 changes: 35 additions & 17 deletions src/treecipe/src/RecipeService/RecipeService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,32 +95,50 @@ export class RecipeService {
return fakeRecipeValue;

case 'multiselectpicklist':

if ( !(xmlFieldDetail.picklistValues) ) {
// THIS SCENARIO INDICATEDS THAT THE PICKLIST FIELD UTILIZED A GLOBAL VALUE SET
const emptyMultiSelectXMLDetailPlaceholder = this.fakerService.getMultipicklistTODOPlaceholderWithExample();
return emptyMultiSelectXMLDetailPlaceholder;
}
const availablePicklistChoices = xmlFieldDetail.picklistValues.map(picklistOption => picklistOption.picklistOptionApiName);
fakeRecipeValue = this.fakerService.buildMultiSelectPicklistRecipeValueByXMLFieldDetail(availablePicklistChoices,
fakeRecipeValue = this.fakerService.buildMultiSelectPicklistRecipeValueByXMLFieldDetail(availablePicklistChoices,
recordTypeApiToRecordTypeWrapperMap,
xmlFieldDetail.apiName
);

return fakeRecipeValue;

// case 'masterdetail':

// return {
// type: 'lookup'
// };

// case 'lookup':

// return 'test';

default:


case 'text':
case 'textarea':
case 'longtextarea':
case 'html':

if (xmlFieldDetail.length) {
fakeRecipeValue = this.fakerService.buildTextRecipeValueWithLength(xmlFieldDetail.length);
return fakeRecipeValue;
}
// Fall through to default if no length

case 'number':
case 'percent':

if (xmlFieldDetail.precision) {
fakeRecipeValue = this.fakerService.buildNumericRecipeValueWithPrecisionAndScale(xmlFieldDetail.precision, xmlFieldDetail.scale);
return fakeRecipeValue;
}
// Fall through to default if no precision

case 'currency':

if (xmlFieldDetail.precision) {
fakeRecipeValue = this.fakerService.buildCurrencyRecipeValueWithPrecisionAndScale(xmlFieldDetail.precision, xmlFieldDetail.scale);
return fakeRecipeValue;
}
// Fall through to default if no precision

default:

fakeRecipeValue = this.getFakeValueIfExpectedSalesforceFieldType(fieldType);
return fakeRecipeValue;

Expand Down Expand Up @@ -296,4 +314,4 @@ ${this.generateTabs(1)}${fieldPropertAndRecipeValue}`;

}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -191,9 +191,9 @@ describe('FakerJSRecipeService IRecipeService Implementation Shared Intstance Te
test('given expected number XMLFieldDetail, returns the expected fakerJS YAML recipe value', () => {

const expectedXMLDetailForNumber:XMLFieldDetail = XMLMarkupMockService.getNumberXMLFieldDetail();
const expectedFakerJSExpressionForNumber = `|
\${{ faker.number.int({min: 0, max: 999999}) }}`;
const recordTypeNameByRecordTypeNameToXMLMarkup = {};
const expectedFakerJSExpressionForNumber = `|
\${{ faker.number.int({min: 0, max: 999999999999999999}) }}`;
const recordTypeNameByRecordTypeNameToXMLMarkup = {};

const actualFakerJSForNumber = recipeServiceWithFakerJS.getRecipeFakeValueByXMLFieldDetail(expectedXMLDetailForNumber, recordTypeNameByRecordTypeNameToXMLMarkup);
expect(actualFakerJSForNumber).toBe(expectedFakerJSExpressionForNumber);
Expand All @@ -203,7 +203,8 @@ describe('FakerJSRecipeService IRecipeService Implementation Shared Intstance Te
test('given expected currency XMLFieldDetail, returns the expected fakerJS YAML recipe value', () => {

const expectedXMLDetailForCurrency:XMLFieldDetail = XMLMarkupMockService.getCurrencyFieldDetail();
const expectedFakerJSExpressionForCurrency = "\${{ faker.finance.amount(0, 999999, 2) }}";
const expectedFakerJSExpressionForCurrency = `|
\${{ faker.finance.amount({min: 0, max: 9999999999999999, dec: 2}) }}`;
const recordTypeNameByRecordTypeNameToXMLMarkup = {};
const actualFakerJSForCurrency = recipeServiceWithFakerJS.getRecipeFakeValueByXMLFieldDetail(expectedXMLDetailForCurrency, recordTypeNameByRecordTypeNameToXMLMarkup);

Expand Down
18 changes: 14 additions & 4 deletions src/treecipe/src/RecipeService/tests/RecipeService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,8 @@ describe('SnowfakeryRecipeService IRecipeService Implementation Shared Intstance
test('given expected number XMLFieldDetail, returns the expected snowfakery YAML recipe value', () => {

const expectedXMLDetailForNumber:XMLFieldDetail = XMLMarkupMockService.getNumberXMLFieldDetail();
const expectedSnowfakeryValueForNumber = '${{fake.random_int(min=0, max=999999)}}';
const recordTypeNameByRecordTypeNameToXMLMarkup = {};
const expectedSnowfakeryValueForNumber = '${{fake.random_int(min=0, max=999999999999999999)}}';
const recordTypeNameByRecordTypeNameToXMLMarkup = {};

const actualSnowfakeryValueForNumber = recipeServiceWithSnow.getRecipeFakeValueByXMLFieldDetail(expectedXMLDetailForNumber, recordTypeNameByRecordTypeNameToXMLMarkup);

Expand All @@ -136,14 +136,25 @@ describe('SnowfakeryRecipeService IRecipeService Implementation Shared Intstance
test('given expected currency XMLFieldDetail, returns the expected snowfakery YAML recipe value', () => {

const expectedXMLDetailForCurrency:XMLFieldDetail = XMLMarkupMockService.getCurrencyFieldDetail();
const expectedSnowfakeryValueForCurrency = '${{fake.pydecimal(left_digits=6, right_digits=2, positive=True)}}';
const expectedSnowfakeryValueForCurrency = '${{fake.pydecimal(left_digits=16, right_digits=2, positive=True)}}';
const recordTypeNameByRecordTypeNameToXMLMarkup = {};
const actualSnowfakeryValueForCurrency = recipeServiceWithSnow.getRecipeFakeValueByXMLFieldDetail(expectedXMLDetailForCurrency, recordTypeNameByRecordTypeNameToXMLMarkup);

expect(actualSnowfakeryValueForCurrency).toBe(expectedSnowfakeryValueForCurrency);

});

test('given expected text XMLFieldDetail with length, returns the expected snowfakery YAML recipe value with length limit', () => {

const expectedXMLDetailForTextWithLength:XMLFieldDetail = XMLMarkupMockService.getTextXMLFieldDetailWithLength();
const expectedSnowfakeryValueForTextWithLength = '${{fake.text(max_nb_chars=50)}}';
const recordTypeNameByRecordTypeNameToXMLMarkup = {};
const actualSnowfakeryValueForTextWithLength = recipeServiceWithSnow.getRecipeFakeValueByXMLFieldDetail(expectedXMLDetailForTextWithLength, recordTypeNameByRecordTypeNameToXMLMarkup);

expect(actualSnowfakeryValueForTextWithLength).toBe(expectedSnowfakeryValueForTextWithLength);

});

});

describe('initiateRecipeByObjectName', () => {
Expand Down Expand Up @@ -407,4 +418,3 @@ describe('SnowfakeryRecipeService IRecipeService Implementation Shared Intstance
});

});

5 changes: 4 additions & 1 deletion src/treecipe/src/XMLProcessingService/XMLFieldDetail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,7 @@ export class XMLFieldDetail {
public controllingField?: string;
public xmlMarkup: string;
public isStandardValueSet?: boolean;
}
public precision?: number;
public scale?: number;
public length?: number;
}
21 changes: 16 additions & 5 deletions src/treecipe/src/XMLProcessingService/XmlFileProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,22 @@ export class XmlFileProcessor {
xmlFieldDetail.fieldType = typeValue;
xmlFieldDetail.fieldLabel = fieldLabel;

// Parse precision, scale, and length properties (only if they exist in XML)
const precision = fieldXML?.CustomField?.precision?.[0];
if (precision !== undefined && precision !== null) {
xmlFieldDetail.precision = parseInt(precision, 10);
}

const scale = fieldXML?.CustomField?.scale?.[0];
if (scale !== undefined && scale !== null) {
xmlFieldDetail.scale = parseInt(scale, 10);
}

const length = fieldXML?.CustomField?.length?.[0];
if (length !== undefined && length !== null) {
xmlFieldDetail.length = parseInt(length, 10);
}

if ( typeValue === 'Picklist' || typeValue === "MultiselectPicklist") {

let picklistValueSetMarkup = fieldXML.CustomField.valueSet?.[0];
Expand Down Expand Up @@ -215,8 +231,3 @@ export class XmlFileProcessor {


}





Loading
Loading