Skip to content

Commit d2d4193

Browse files
authored
Feature/text field precision (#35)
* feat: add methods for building text and numeric recipe values with constraints - Added buildTextRecipeValueWithLength(length: number) and buildNumericRecipeValueWithPrecision(precision: number) to IRecipeFakerService interface - Implemented methods in FakerJSRecipeFakerService and SnowfakeryRecipeFakerService to generate faker data respecting length/precision limits - Updated RecipeService to utilize new methods for improved fake data generation in text and numeric fields * feat(recipe-faker): add scale support to numeric recipe value generation - Renamed `buildNumericRecipeValueWithPrecision` to `buildNumericRecipeValueWithPrecisionAndScale` in IRecipeFakerService and implementations - Added optional `scale` parameter to handle decimal fields (currency, percent) with appropriate precision and decimal places - Updated FakerJS and Snowfakery services to generate integers for scale=0 and decimals for scale>0 - Modified RecipeService to pass scale for percent fields, ensuring accurate fake data generation for numeric types * fix(faker): cap numeric precision at 15 to avoid JavaScript number overflow Updated FakerJSRecipeFakerService and SnowfakeryRecipeFakerService to limit precision to 15 digits for integer and decimal fields, preventing loss of accuracy in large numbers. Adjusted test expectations in FakerJSRecipeService.test.ts to reflect the new max value (10^15 - 1) for integer fields. * refactor: simplify numeric value generation and add currency-specific method - Refactored buildNumericRecipeValueWithPrecisionAndScale in FakerJS and Snowfakery services to remove redundant calculations and improve readability. - Added buildCurrencyRecipeValueWithPrecisionAndScale method to handle currency fields separately, ensuring full precision usage. - Updated IRecipeFakerService interface to include the new currency method. * feat: format faker expressions as YAML block scalars in recipes Update buildPercentRecipeValueWithPrecisionAndScale and buildCurrencyRecipeValueWithPrecisionAndScale to prepend `|` and add indentation to the returned strings, ensuring they are valid YAML literal block scalars. Adjust corresponding test expectations to match the new formatting for accurate assertion in FakerJS recipe service tests. This improves YAML recipe generation by preserving multi-line structure in generated fake values. * fix: correct max value calculation for numeric and currency fields Refactor the buildNumericRecipeValueWithPrecisionAndScale and buildCurrencyRecipeValueWithPrecisionAndScale methods to use precision minus scale as the digit count left of the decimal for max value generation, fixing inaccurate faker outputs for decimal fields. Updated corresponding test expectations. * fix(RecipeFakerService): correctly calculate left_digits for numeric and currency fields based on precision - scale - Updated buildNumericRecipeValueWithPrecisionAndScale and buildCurrencyRecipeValueWithPrecisionAndScale to use left_digits = precision - effectiveScale, ensuring accurate representation of digits before the decimal point. - Adjusted the corresponding test to expect left_digits=16 for a currency field with 18 precision and 2 scale, matching the correct calculation. This fixes a potential issue where total precision was mistakenly used as left_digits, leading to incorrect fake data generation. * fix: add missing closing braces and remove comments in SnowfakeryRecipeFakerService Removed inline comments from buildNumericRecipeValueWithPrecisionAndScale and buildCurrencyRecipeValueWithPrecisionAndScale methods, and added missing closing braces to fix syntax errors. * feat: Enhance numeric field precision and fix related bugs - Add support for scale in numeric value generation for accurate currency and number fields - Format faker expressions as YAML block scalars and add constraints for text/numeric values - Fix max value calculations, precision-based left digits, JS overflow, and code cleanup - Bump version to 2.7.0 * docs: clean up changelog by removing preliminary unreleased features and fixes Remove entries for numeric scale support, YAML formatting, value constraints, and bug fixes in numeric handling to reflect accurate pre-release state, preventing confusion in release notes.
1 parent bddf457 commit d2d4193

File tree

12 files changed

+325
-66
lines changed

12 files changed

+325
-66
lines changed

CHANGELOG.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,46 @@
11
# Change Log
22

3+
## [2.7.0] [PR#35](https://github.com/jdschleicher/Salesforce-Data-Treecipe/pull/35) - Feature: Enhanced Text & Numeric Field Precision Handling
4+
5+
### 🎯 Major Features
6+
7+
#### 1. **Text and Numeric Value Constraints**
8+
Added new methods for building text and numeric recipe values with constraints, enhancing control over generated data ranges and formats.
9+
10+
### 🔧 Technical Details
11+
12+
**Code Example - Currency Field XML to FakerJS YAML**:
13+
14+
Given a custom object field XML markup with precision and scale:
15+
16+
```xml
17+
<fields>
18+
<fullName>Price__c</fullName>
19+
<description>Product price</description>
20+
<externalId>false</externalId>
21+
<label>Price</label>
22+
<precision>8</precision>
23+
<required>false</required>
24+
<scale>2</scale>
25+
<type>Currency</type>
26+
<unique>false</unique>
27+
</fields>
28+
```
29+
30+
The generated recipe automatically creates a faker expression that respects the precision (8) and scale (2):
31+
32+
```yaml
33+
- object: My_Custom_Object__c
34+
fields:
35+
Price__c: ${{ faker.finance.amount({ min: 0, max: 999999, dec: 2 }) }}
36+
```
37+
38+
This ensures the generated currency values:
39+
- Have at most 8 total digits (precision)
40+
- Have exactly 2 decimal places (scale)
41+
- Are within the valid range (0 to 99,999.99)
42+
- Match Salesforce field constraints
43+
344
## [2.6.0] [PR#34](https://github.com/jdschleicher/Salesforce-Data-Treecipe/pull/34) - Feature: Relationship Service & Bug Fix for Special Characters in Picklists
445
546
### 🎯 Major Features

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"displayName": "Salesforce Data Treecipe",
44
"description": "source-fidelity driven development, pairs well with cumulus-ci",
55
"icon": "images/datatreecipe.webp",
6-
"version": "2.6.0",
6+
"version": "2.7.0",
77
"engines": {
88
"vscode": "^1.94.0"
99
},

src/treecipe/src/RecipeFakerService.ts/FakerJSRecipeFakerService/FakerJSRecipeFakerService.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -504,7 +504,37 @@ ${this.generateTabs(5)}${randomChoicesBreakdown}`;
504504
getMultipicklistTODOPlaceholderWithExample():string {
505505

506506
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(';') }}`;
507-
return emptyMultiSelectXMLDetailPlaceholder;
507+
return emptyMultiSelectXMLDetailPlaceholder;
508+
509+
}
510+
511+
buildTextRecipeValueWithLength(length: number): string {
512+
return `${this.openingRecipeSyntax} faker.lorem.text(${length}).substring(0, ${length}) ${this.closingRecipeSyntax}`;
513+
}
514+
515+
buildNumericRecipeValueWithPrecisionAndScale(precision: number, scale?: number): string {
516+
517+
const effectiveScale = scale ?? 0;
518+
const maxNumbersLeftOfDecimal = '9'.repeat(precision - effectiveScale);
519+
520+
if (effectiveScale === 0) {
521+
return `|
522+
${this.openingRecipeSyntax} faker.number.int({min: 0, max: ${maxNumbersLeftOfDecimal}}) ${this.closingRecipeSyntax}`;
523+
} else {
524+
return `|
525+
${this.openingRecipeSyntax} faker.finance.amount({min: 0, max: ${maxNumbersLeftOfDecimal}, dec: ${effectiveScale}}) ${this.closingRecipeSyntax}`;
526+
}
527+
528+
}
529+
530+
buildCurrencyRecipeValueWithPrecisionAndScale(precision: number, scale?: number): string {
531+
// Special handling for currency fields - use full precision as left_digits
532+
533+
const effectiveScale = scale ?? 0;
534+
const maxNumbersLeftOfDecimal = '9'.repeat(precision - effectiveScale);
535+
536+
return `|
537+
${this.openingRecipeSyntax} faker.finance.amount({min: 0, max: ${maxNumbersLeftOfDecimal}, dec: ${effectiveScale}}) ${this.closingRecipeSyntax}`;
508538

509539
}
510540

src/treecipe/src/RecipeFakerService.ts/IRecipeFakerService.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,7 @@ export interface IRecipeFakerService {
2828
controllingValue: string): string
2929
getMultipicklistTODOPlaceholderWithExample(): string
3030
getStandardAndGlobalValueSetTODOPlaceholderWithExample(): string
31-
}
31+
buildTextRecipeValueWithLength(length: number): string
32+
buildNumericRecipeValueWithPrecisionAndScale(precision: number, scale?: number): string
33+
buildCurrencyRecipeValueWithPrecisionAndScale(precision: number, scale?: number): string
34+
}

src/treecipe/src/RecipeFakerService.ts/SnowfakeryRecipeFakerService/SnowfakeryRecipeFakerService.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -397,7 +397,32 @@ ${this.generateTabs(5)}${randomChoicesBreakdown}`;
397397

398398
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')))) }}`;
399399
return emptyMultiSelectXMLDetailPlaceholder;
400+
401+
}
402+
403+
buildTextRecipeValueWithLength(length: number): string {
404+
return `${this.openingRecipeSyntax}fake.text(max_nb_chars=${length})${this.closingRecipeSyntax}`;
405+
}
406+
407+
buildNumericRecipeValueWithPrecisionAndScale(precision: number, scale?: number): string {
408+
409+
const effectiveScale = scale ?? 0;
410+
const maxNumbersLeftOfDecimal = '9'.repeat(precision - effectiveScale);
411+
412+
if (effectiveScale === 0) {
413+
return `${this.openingRecipeSyntax}fake.random_int(min=0, max=${maxNumbersLeftOfDecimal})${this.closingRecipeSyntax}`;
414+
} else {
415+
return `${this.openingRecipeSyntax}fake.pydecimal(left_digits=${maxNumbersLeftOfDecimal}, right_digits=${effectiveScale}, positive=True)${this.closingRecipeSyntax}`;
416+
}
417+
418+
}
419+
420+
buildCurrencyRecipeValueWithPrecisionAndScale(precision: number, scale?: number): string {
400421

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

403-
}
428+
}

src/treecipe/src/RecipeService/RecipeService.ts

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -95,32 +95,50 @@ export class RecipeService {
9595
return fakeRecipeValue;
9696

9797
case 'multiselectpicklist':
98-
98+
9999
if ( !(xmlFieldDetail.picklistValues) ) {
100100
// THIS SCENARIO INDICATEDS THAT THE PICKLIST FIELD UTILIZED A GLOBAL VALUE SET
101101
const emptyMultiSelectXMLDetailPlaceholder = this.fakerService.getMultipicklistTODOPlaceholderWithExample();
102102
return emptyMultiSelectXMLDetailPlaceholder;
103103
}
104104
const availablePicklistChoices = xmlFieldDetail.picklistValues.map(picklistOption => picklistOption.picklistOptionApiName);
105-
fakeRecipeValue = this.fakerService.buildMultiSelectPicklistRecipeValueByXMLFieldDetail(availablePicklistChoices,
105+
fakeRecipeValue = this.fakerService.buildMultiSelectPicklistRecipeValueByXMLFieldDetail(availablePicklistChoices,
106106
recordTypeApiToRecordTypeWrapperMap,
107107
xmlFieldDetail.apiName
108108
);
109-
109+
110110
return fakeRecipeValue;
111-
112-
// case 'masterdetail':
113-
114-
// return {
115-
// type: 'lookup'
116-
// };
117-
118-
// case 'lookup':
119-
120-
// return 'test';
121-
122-
default:
123-
111+
112+
case 'text':
113+
case 'textarea':
114+
case 'longtextarea':
115+
case 'html':
116+
117+
if (xmlFieldDetail.length) {
118+
fakeRecipeValue = this.fakerService.buildTextRecipeValueWithLength(xmlFieldDetail.length);
119+
return fakeRecipeValue;
120+
}
121+
// Fall through to default if no length
122+
123+
case 'number':
124+
case 'percent':
125+
126+
if (xmlFieldDetail.precision) {
127+
fakeRecipeValue = this.fakerService.buildNumericRecipeValueWithPrecisionAndScale(xmlFieldDetail.precision, xmlFieldDetail.scale);
128+
return fakeRecipeValue;
129+
}
130+
// Fall through to default if no precision
131+
132+
case 'currency':
133+
134+
if (xmlFieldDetail.precision) {
135+
fakeRecipeValue = this.fakerService.buildCurrencyRecipeValueWithPrecisionAndScale(xmlFieldDetail.precision, xmlFieldDetail.scale);
136+
return fakeRecipeValue;
137+
}
138+
// Fall through to default if no precision
139+
140+
default:
141+
124142
fakeRecipeValue = this.getFakeValueIfExpectedSalesforceFieldType(fieldType);
125143
return fakeRecipeValue;
126144

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

297315
}
298316

299-
}
317+
}

src/treecipe/src/RecipeService/tests/FakerJSRecipeService.test.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -191,9 +191,9 @@ describe('FakerJSRecipeService IRecipeService Implementation Shared Intstance Te
191191
test('given expected number XMLFieldDetail, returns the expected fakerJS YAML recipe value', () => {
192192

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

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

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

src/treecipe/src/RecipeService/tests/RecipeService.test.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -124,8 +124,8 @@ describe('SnowfakeryRecipeService IRecipeService Implementation Shared Intstance
124124
test('given expected number XMLFieldDetail, returns the expected snowfakery YAML recipe value', () => {
125125

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

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

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

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

143143
expect(actualSnowfakeryValueForCurrency).toBe(expectedSnowfakeryValueForCurrency);
144144

145145
});
146146

147+
test('given expected text XMLFieldDetail with length, returns the expected snowfakery YAML recipe value with length limit', () => {
148+
149+
const expectedXMLDetailForTextWithLength:XMLFieldDetail = XMLMarkupMockService.getTextXMLFieldDetailWithLength();
150+
const expectedSnowfakeryValueForTextWithLength = '${{fake.text(max_nb_chars=50)}}';
151+
const recordTypeNameByRecordTypeNameToXMLMarkup = {};
152+
const actualSnowfakeryValueForTextWithLength = recipeServiceWithSnow.getRecipeFakeValueByXMLFieldDetail(expectedXMLDetailForTextWithLength, recordTypeNameByRecordTypeNameToXMLMarkup);
153+
154+
expect(actualSnowfakeryValueForTextWithLength).toBe(expectedSnowfakeryValueForTextWithLength);
155+
156+
});
157+
147158
});
148159

149160
describe('initiateRecipeByObjectName', () => {
@@ -407,4 +418,3 @@ describe('SnowfakeryRecipeService IRecipeService Implementation Shared Intstance
407418
});
408419

409420
});
410-

src/treecipe/src/XMLProcessingService/XMLFieldDetail.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,7 @@ export class XMLFieldDetail {
1010
public controllingField?: string;
1111
public xmlMarkup: string;
1212
public isStandardValueSet?: boolean;
13-
}
13+
public precision?: number;
14+
public scale?: number;
15+
public length?: number;
16+
}

src/treecipe/src/XMLProcessingService/XmlFileProcessor.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,22 @@ export class XmlFileProcessor {
3737
xmlFieldDetail.fieldType = typeValue;
3838
xmlFieldDetail.fieldLabel = fieldLabel;
3939

40+
// Parse precision, scale, and length properties (only if they exist in XML)
41+
const precision = fieldXML?.CustomField?.precision?.[0];
42+
if (precision !== undefined && precision !== null) {
43+
xmlFieldDetail.precision = parseInt(precision, 10);
44+
}
45+
46+
const scale = fieldXML?.CustomField?.scale?.[0];
47+
if (scale !== undefined && scale !== null) {
48+
xmlFieldDetail.scale = parseInt(scale, 10);
49+
}
50+
51+
const length = fieldXML?.CustomField?.length?.[0];
52+
if (length !== undefined && length !== null) {
53+
xmlFieldDetail.length = parseInt(length, 10);
54+
}
55+
4056
if ( typeValue === 'Picklist' || typeValue === "MultiselectPicklist") {
4157

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

216232

217233
}
218-
219-
220-
221-
222-

0 commit comments

Comments
 (0)