From 46c86487286d4243bf752d42464bd7fb045b836f Mon Sep 17 00:00:00 2001 From: jdschleicher Date: Sat, 15 Nov 2025 08:13:47 -0500 Subject: [PATCH 01/10] 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 --- .../FakerJSRecipeFakerService.ts | 12 ++++- .../IRecipeFakerService.ts | 4 +- .../SnowfakeryRecipeFakerService.ts | 14 +++++- .../src/RecipeService/RecipeService.ts | 45 ++++++++++++------- .../RecipeService/tests/RecipeService.test.ts | 12 ++++- .../XMLProcessingService/XMLFieldDetail.ts | 4 +- .../tests/mocks/XMLMarkupMockService.ts | 33 +++++++++++++- 7 files changed, 100 insertions(+), 24 deletions(-) diff --git a/src/treecipe/src/RecipeFakerService.ts/FakerJSRecipeFakerService/FakerJSRecipeFakerService.ts b/src/treecipe/src/RecipeFakerService.ts/FakerJSRecipeFakerService/FakerJSRecipeFakerService.ts index 2b381c0..b48f2a2 100644 --- a/src/treecipe/src/RecipeFakerService.ts/FakerJSRecipeFakerService/FakerJSRecipeFakerService.ts +++ b/src/treecipe/src/RecipeFakerService.ts/FakerJSRecipeFakerService/FakerJSRecipeFakerService.ts @@ -504,8 +504,18 @@ ${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}`; + } + + buildNumericRecipeValueWithPrecision(precision: number): string { + // For numeric fields, precision typically represents total digits, but we can use it to limit the range + const maxValue = Math.pow(10, precision) - 1; + return `${this.openingRecipeSyntax} faker.number.int({min: 0, max: ${maxValue}}) ${this.closingRecipeSyntax}`; + } + } diff --git a/src/treecipe/src/RecipeFakerService.ts/IRecipeFakerService.ts b/src/treecipe/src/RecipeFakerService.ts/IRecipeFakerService.ts index 87aa6c1..8f5d23a 100644 --- a/src/treecipe/src/RecipeFakerService.ts/IRecipeFakerService.ts +++ b/src/treecipe/src/RecipeFakerService.ts/IRecipeFakerService.ts @@ -28,4 +28,6 @@ export interface IRecipeFakerService { controllingValue: string): string getMultipicklistTODOPlaceholderWithExample(): string getStandardAndGlobalValueSetTODOPlaceholderWithExample(): string -} \ No newline at end of file + buildTextRecipeValueWithLength(length: number): string + buildNumericRecipeValueWithPrecision(precision: number): string +} diff --git a/src/treecipe/src/RecipeFakerService.ts/SnowfakeryRecipeFakerService/SnowfakeryRecipeFakerService.ts b/src/treecipe/src/RecipeFakerService.ts/SnowfakeryRecipeFakerService/SnowfakeryRecipeFakerService.ts index 07c90bc..89f3ee8 100644 --- a/src/treecipe/src/RecipeFakerService.ts/SnowfakeryRecipeFakerService/SnowfakeryRecipeFakerService.ts +++ b/src/treecipe/src/RecipeFakerService.ts/SnowfakeryRecipeFakerService/SnowfakeryRecipeFakerService.ts @@ -397,7 +397,17 @@ ${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}`; + } + + buildNumericRecipeValueWithPrecision(precision: number): string { + // For numeric fields, precision typically represents total digits, but we can use it to limit the range + const maxValue = Math.pow(10, precision) - 1; + return `${this.openingRecipeSyntax}fake.random_int(min=0, max=${maxValue})${this.closingRecipeSyntax}`; } -} \ No newline at end of file +} diff --git a/src/treecipe/src/RecipeService/RecipeService.ts b/src/treecipe/src/RecipeService/RecipeService.ts index 25e2730..f627c33 100644 --- a/src/treecipe/src/RecipeService/RecipeService.ts +++ b/src/treecipe/src/RecipeService/RecipeService.ts @@ -95,32 +95,43 @@ 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 'currency': + case 'percent': + + if (xmlFieldDetail.precision) { + fakeRecipeValue = this.fakerService.buildNumericRecipeValueWithPrecision(xmlFieldDetail.precision); + return fakeRecipeValue; + } + // Fall through to default if no precision + + default: + fakeRecipeValue = this.getFakeValueIfExpectedSalesforceFieldType(fieldType); return fakeRecipeValue; @@ -296,4 +307,4 @@ ${this.generateTabs(1)}${fieldPropertAndRecipeValue}`; } -} \ No newline at end of file +} diff --git a/src/treecipe/src/RecipeService/tests/RecipeService.test.ts b/src/treecipe/src/RecipeService/tests/RecipeService.test.ts index f0173cf..0b23ba4 100644 --- a/src/treecipe/src/RecipeService/tests/RecipeService.test.ts +++ b/src/treecipe/src/RecipeService/tests/RecipeService.test.ts @@ -144,6 +144,17 @@ describe('SnowfakeryRecipeService IRecipeService Implementation Shared Intstance }); + 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', () => { @@ -407,4 +418,3 @@ describe('SnowfakeryRecipeService IRecipeService Implementation Shared Intstance }); }); - diff --git a/src/treecipe/src/XMLProcessingService/XMLFieldDetail.ts b/src/treecipe/src/XMLProcessingService/XMLFieldDetail.ts index f4e39a8..2a30f13 100644 --- a/src/treecipe/src/XMLProcessingService/XMLFieldDetail.ts +++ b/src/treecipe/src/XMLProcessingService/XMLFieldDetail.ts @@ -10,4 +10,6 @@ export class XMLFieldDetail { public controllingField?: string; public xmlMarkup: string; public isStandardValueSet?: boolean; -} \ No newline at end of file + public precision?: number; + public length?: number; +} diff --git a/src/treecipe/src/XMLProcessingService/tests/mocks/XMLMarkupMockService.ts b/src/treecipe/src/XMLProcessingService/tests/mocks/XMLMarkupMockService.ts index b9c1e10..b554410 100644 --- a/src/treecipe/src/XMLProcessingService/tests/mocks/XMLMarkupMockService.ts +++ b/src/treecipe/src/XMLProcessingService/tests/mocks/XMLMarkupMockService.ts @@ -1509,7 +1509,7 @@ export class XMLMarkupMockService { } static getTextXMLFieldDetail():XMLFieldDetail { - + let textXMLFieldDetail: XMLFieldDetail = { fieldType: "Text", apiName: "Text__c", @@ -1520,6 +1520,37 @@ export class XMLMarkupMockService { return textXMLFieldDetail; } + static getTextXMLFieldDetailWithLength():XMLFieldDetail { + + let textXMLFieldDetail: XMLFieldDetail = { + fieldType: "Text", + apiName: "TextWithLength__c", + fieldLabel: "Text With Length", + length: 50, + xmlMarkup: this.getTextFieldTypeWithLengthXMLMarkup() + }; + + return textXMLFieldDetail; + } + + static getTextFieldTypeWithLengthXMLMarkup():string { + const xmlTextMarkup = ` + + + TextWithLength__c + false + + 50 + false + false + Text + false + +`; + return xmlTextMarkup; + + } + static getParseStringCLEGlobalValueSetMock() { const parseCLEGlobalAny = { GlobalValueSet: { From 8396880e5b3b9c20f6ea1f7b0d064c1c7ad264ca Mon Sep 17 00:00:00 2001 From: jdschleicher Date: Sat, 15 Nov 2025 08:51:31 -0500 Subject: [PATCH 02/10] 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 --- .../FakerJSRecipeFakerService.ts | 17 +++++++++++++---- .../IRecipeFakerService.ts | 2 +- .../SnowfakeryRecipeFakerService.ts | 17 +++++++++++++---- src/treecipe/src/RecipeService/RecipeService.ts | 2 +- .../src/XMLProcessingService/XMLFieldDetail.ts | 1 + 5 files changed, 29 insertions(+), 10 deletions(-) diff --git a/src/treecipe/src/RecipeFakerService.ts/FakerJSRecipeFakerService/FakerJSRecipeFakerService.ts b/src/treecipe/src/RecipeFakerService.ts/FakerJSRecipeFakerService/FakerJSRecipeFakerService.ts index b48f2a2..2d87a28 100644 --- a/src/treecipe/src/RecipeFakerService.ts/FakerJSRecipeFakerService/FakerJSRecipeFakerService.ts +++ b/src/treecipe/src/RecipeFakerService.ts/FakerJSRecipeFakerService/FakerJSRecipeFakerService.ts @@ -512,10 +512,19 @@ ${this.generateTabs(5)}${randomChoicesBreakdown}`; return `${this.openingRecipeSyntax} faker.lorem.text(${length}).substring(0, ${length}) ${this.closingRecipeSyntax}`; } - buildNumericRecipeValueWithPrecision(precision: number): string { - // For numeric fields, precision typically represents total digits, but we can use it to limit the range - const maxValue = Math.pow(10, precision) - 1; - return `${this.openingRecipeSyntax} faker.number.int({min: 0, max: ${maxValue}}) ${this.closingRecipeSyntax}`; + buildNumericRecipeValueWithPrecisionAndScale(precision: number, scale?: number): string { + // Handle all numeric fields (number, currency, percent) with precision and optional scale + const effectiveScale = scale ?? 0; + + if (effectiveScale === 0) { + // Integer fields (number with scale=0, or scale not specified) + const maxValue = Math.pow(10, precision) - 1; + return `${this.openingRecipeSyntax} faker.number.int({min: 0, max: ${maxValue}}) ${this.closingRecipeSyntax}`; + } else { + // Decimal fields (currency, percent with scale > 0) + const maxValue = Math.pow(10, precision - effectiveScale) - Math.pow(10, -effectiveScale); + return `${this.openingRecipeSyntax} faker.finance.amount({min: 0, max: ${maxValue}, dec: ${effectiveScale}}) ${this.closingRecipeSyntax}`; + } } } diff --git a/src/treecipe/src/RecipeFakerService.ts/IRecipeFakerService.ts b/src/treecipe/src/RecipeFakerService.ts/IRecipeFakerService.ts index 8f5d23a..48085fb 100644 --- a/src/treecipe/src/RecipeFakerService.ts/IRecipeFakerService.ts +++ b/src/treecipe/src/RecipeFakerService.ts/IRecipeFakerService.ts @@ -29,5 +29,5 @@ export interface IRecipeFakerService { getMultipicklistTODOPlaceholderWithExample(): string getStandardAndGlobalValueSetTODOPlaceholderWithExample(): string buildTextRecipeValueWithLength(length: number): string - buildNumericRecipeValueWithPrecision(precision: number): string + buildNumericRecipeValueWithPrecisionAndScale(precision: number, scale?: number): string } diff --git a/src/treecipe/src/RecipeFakerService.ts/SnowfakeryRecipeFakerService/SnowfakeryRecipeFakerService.ts b/src/treecipe/src/RecipeFakerService.ts/SnowfakeryRecipeFakerService/SnowfakeryRecipeFakerService.ts index 89f3ee8..d0caaee 100644 --- a/src/treecipe/src/RecipeFakerService.ts/SnowfakeryRecipeFakerService/SnowfakeryRecipeFakerService.ts +++ b/src/treecipe/src/RecipeFakerService.ts/SnowfakeryRecipeFakerService/SnowfakeryRecipeFakerService.ts @@ -404,10 +404,19 @@ ${this.generateTabs(5)}${randomChoicesBreakdown}`; return `${this.openingRecipeSyntax}fake.text(max_nb_chars=${length})${this.closingRecipeSyntax}`; } - buildNumericRecipeValueWithPrecision(precision: number): string { - // For numeric fields, precision typically represents total digits, but we can use it to limit the range - const maxValue = Math.pow(10, precision) - 1; - return `${this.openingRecipeSyntax}fake.random_int(min=0, max=${maxValue})${this.closingRecipeSyntax}`; + buildNumericRecipeValueWithPrecisionAndScale(precision: number, scale?: number): string { + // Handle all numeric fields (number, currency, percent) with precision and optional scale + const effectiveScale = scale ?? 0; + + if (effectiveScale === 0) { + // Integer fields (number with scale=0, or scale not specified) + const maxValue = Math.pow(10, precision) - 1; + return `${this.openingRecipeSyntax}fake.random_int(min=0, max=${maxValue})${this.closingRecipeSyntax}`; + } else { + // Decimal fields (currency, percent with scale > 0) + const maxValue = Math.pow(10, precision - effectiveScale) - Math.pow(10, -effectiveScale); + return `${this.openingRecipeSyntax}fake.pydecimal(left_digits=${precision - effectiveScale}, right_digits=${effectiveScale}, positive=True)${this.closingRecipeSyntax}`; + } } } diff --git a/src/treecipe/src/RecipeService/RecipeService.ts b/src/treecipe/src/RecipeService/RecipeService.ts index f627c33..be41282 100644 --- a/src/treecipe/src/RecipeService/RecipeService.ts +++ b/src/treecipe/src/RecipeService/RecipeService.ts @@ -125,7 +125,7 @@ export class RecipeService { case 'percent': if (xmlFieldDetail.precision) { - fakeRecipeValue = this.fakerService.buildNumericRecipeValueWithPrecision(xmlFieldDetail.precision); + fakeRecipeValue = this.fakerService.buildNumericRecipeValueWithPrecisionAndScale(xmlFieldDetail.precision, xmlFieldDetail.scale); return fakeRecipeValue; } // Fall through to default if no precision diff --git a/src/treecipe/src/XMLProcessingService/XMLFieldDetail.ts b/src/treecipe/src/XMLProcessingService/XMLFieldDetail.ts index 2a30f13..f4f5d80 100644 --- a/src/treecipe/src/XMLProcessingService/XMLFieldDetail.ts +++ b/src/treecipe/src/XMLProcessingService/XMLFieldDetail.ts @@ -11,5 +11,6 @@ export class XMLFieldDetail { public xmlMarkup: string; public isStandardValueSet?: boolean; public precision?: number; + public scale?: number; public length?: number; } From 02dad19033468f916296f40cd3bb538b7836424d Mon Sep 17 00:00:00 2001 From: jdschleicher Date: Mon, 17 Nov 2025 05:33:11 -0500 Subject: [PATCH 03/10] 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. --- .../FakerJSRecipeFakerService.ts | 11 ++- .../SnowfakeryRecipeFakerService.ts | 11 ++- .../tests/FakerJSRecipeService.test.ts | 7 +- .../RecipeService/tests/RecipeService.test.ts | 6 +- .../XMLProcessingService/XmlFileProcessor.ts | 21 +++- .../tests/XmlFileProcessor.test.ts | 98 ++++++++++++++----- .../tests/mocks/XMLMarkupMockService.ts | 48 ++++++++- 7 files changed, 155 insertions(+), 47 deletions(-) diff --git a/src/treecipe/src/RecipeFakerService.ts/FakerJSRecipeFakerService/FakerJSRecipeFakerService.ts b/src/treecipe/src/RecipeFakerService.ts/FakerJSRecipeFakerService/FakerJSRecipeFakerService.ts index 2d87a28..9f1f6a9 100644 --- a/src/treecipe/src/RecipeFakerService.ts/FakerJSRecipeFakerService/FakerJSRecipeFakerService.ts +++ b/src/treecipe/src/RecipeFakerService.ts/FakerJSRecipeFakerService/FakerJSRecipeFakerService.ts @@ -518,11 +518,18 @@ ${this.generateTabs(5)}${randomChoicesBreakdown}`; if (effectiveScale === 0) { // Integer fields (number with scale=0, or scale not specified) - const maxValue = Math.pow(10, precision) - 1; + // Cap precision at 15 to get accurate all-9's result (JavaScript can't represent larger integers) + const safePrecision = Math.min(precision, 18); + const maxValueString = '9'.repeat(safePrecision); + const maxValue = parseInt(maxValueString, 10); return `${this.openingRecipeSyntax} faker.number.int({min: 0, max: ${maxValue}}) ${this.closingRecipeSyntax}`; } else { // Decimal fields (currency, percent with scale > 0) - const maxValue = Math.pow(10, precision - effectiveScale) - Math.pow(10, -effectiveScale); + // For decimal fields, precision includes decimal places, so max integer part is precision - scale + const integerPrecision = precision - effectiveScale; + const safeIntegerPrecision = Math.min(integerPrecision, 18); + const maxValueString = '9'.repeat(safeIntegerPrecision); + const maxValue = parseInt(maxValueString, 10); return `${this.openingRecipeSyntax} faker.finance.amount({min: 0, max: ${maxValue}, dec: ${effectiveScale}}) ${this.closingRecipeSyntax}`; } } diff --git a/src/treecipe/src/RecipeFakerService.ts/SnowfakeryRecipeFakerService/SnowfakeryRecipeFakerService.ts b/src/treecipe/src/RecipeFakerService.ts/SnowfakeryRecipeFakerService/SnowfakeryRecipeFakerService.ts index d0caaee..114a212 100644 --- a/src/treecipe/src/RecipeFakerService.ts/SnowfakeryRecipeFakerService/SnowfakeryRecipeFakerService.ts +++ b/src/treecipe/src/RecipeFakerService.ts/SnowfakeryRecipeFakerService/SnowfakeryRecipeFakerService.ts @@ -410,12 +410,17 @@ ${this.generateTabs(5)}${randomChoicesBreakdown}`; if (effectiveScale === 0) { // Integer fields (number with scale=0, or scale not specified) - const maxValue = Math.pow(10, precision) - 1; + // Create max value by repeating '9' precision times, avoiding Math.pow precision issues + const maxValueString = '9'.repeat(precision); + const maxValue = parseInt(maxValueString, 10); return `${this.openingRecipeSyntax}fake.random_int(min=0, max=${maxValue})${this.closingRecipeSyntax}`; } else { // Decimal fields (currency, percent with scale > 0) - const maxValue = Math.pow(10, precision - effectiveScale) - Math.pow(10, -effectiveScale); - return `${this.openingRecipeSyntax}fake.pydecimal(left_digits=${precision - effectiveScale}, right_digits=${effectiveScale}, positive=True)${this.closingRecipeSyntax}`; + // For decimal fields, precision includes decimal places, so max integer part is precision - scale + const integerPrecision = precision - effectiveScale; + const maxValueString = '9'.repeat(integerPrecision); + const maxValue = parseInt(maxValueString, 10); + return `${this.openingRecipeSyntax}fake.pydecimal(left_digits=${integerPrecision}, right_digits=${effectiveScale}, positive=True)${this.closingRecipeSyntax}`; } } diff --git a/src/treecipe/src/RecipeService/tests/FakerJSRecipeService.test.ts b/src/treecipe/src/RecipeService/tests/FakerJSRecipeService.test.ts index e1d8759..2450d3f 100644 --- a/src/treecipe/src/RecipeService/tests/FakerJSRecipeService.test.ts +++ b/src/treecipe/src/RecipeService/tests/FakerJSRecipeService.test.ts @@ -191,9 +191,8 @@ 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: 999999999999999}) }}`; + const recordTypeNameByRecordTypeNameToXMLMarkup = {}; const actualFakerJSForNumber = recipeServiceWithFakerJS.getRecipeFakeValueByXMLFieldDetail(expectedXMLDetailForNumber, recordTypeNameByRecordTypeNameToXMLMarkup); expect(actualFakerJSForNumber).toBe(expectedFakerJSExpressionForNumber); @@ -203,7 +202,7 @@ 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: 999999999999999, dec: 2}) }}"; const recordTypeNameByRecordTypeNameToXMLMarkup = {}; const actualFakerJSForCurrency = recipeServiceWithFakerJS.getRecipeFakeValueByXMLFieldDetail(expectedXMLDetailForCurrency, recordTypeNameByRecordTypeNameToXMLMarkup); diff --git a/src/treecipe/src/RecipeService/tests/RecipeService.test.ts b/src/treecipe/src/RecipeService/tests/RecipeService.test.ts index 0b23ba4..a1ac717 100644 --- a/src/treecipe/src/RecipeService/tests/RecipeService.test.ts +++ b/src/treecipe/src/RecipeService/tests/RecipeService.test.ts @@ -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=999999999999999)}}'; + const recordTypeNameByRecordTypeNameToXMLMarkup = {}; const actualSnowfakeryValueForNumber = recipeServiceWithSnow.getRecipeFakeValueByXMLFieldDetail(expectedXMLDetailForNumber, recordTypeNameByRecordTypeNameToXMLMarkup); @@ -136,7 +136,7 @@ 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); diff --git a/src/treecipe/src/XMLProcessingService/XmlFileProcessor.ts b/src/treecipe/src/XMLProcessingService/XmlFileProcessor.ts index 152f878..7109019 100644 --- a/src/treecipe/src/XMLProcessingService/XmlFileProcessor.ts +++ b/src/treecipe/src/XMLProcessingService/XmlFileProcessor.ts @@ -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]; @@ -215,8 +231,3 @@ export class XmlFileProcessor { } - - - - - diff --git a/src/treecipe/src/XMLProcessingService/tests/XmlFileProcessor.test.ts b/src/treecipe/src/XMLProcessingService/tests/XmlFileProcessor.test.ts index a81c2c8..2b3b192 100644 --- a/src/treecipe/src/XMLProcessingService/tests/XmlFileProcessor.test.ts +++ b/src/treecipe/src/XMLProcessingService/tests/XmlFileProcessor.test.ts @@ -367,45 +367,93 @@ describe('processXmlFieldContent', () => { }); + test('given text field with length XML tag, parses length property correctly', async () => { -}); + const xmlFieldMarkup = XMLMarkupMockService.getTextFieldTypeWithLengthXMLMarkup(); + const fakeFieldName = 'TextWithLength__c.field-meta.xml'; + const actualFieldDetail = await XmlFileProcessor.processXmlFieldContent(xmlFieldMarkup, fakeFieldName); + const expectedXMLFieldDetail = XMLMarkupMockService.getTextXMLFieldDetailWithLength(); + + expect(actualFieldDetail).toEqual(expectedXMLFieldDetail); + expect(actualFieldDetail.length).toBe(50); + + }); -describe('isXMLFileType', () => { + test('given number field with precision and scale XML tags, parses properties correctly', async () => { - test('given expected xml file extension and valid filetype enum, returns true', () => { + const xmlFieldMarkup = XMLMarkupMockService.getNumberFieldTypeXMLMarkup(); + const fakeFieldName = 'Number__c.field-meta.xml'; + const actualFieldDetail = await XmlFileProcessor.processXmlFieldContent(xmlFieldMarkup, fakeFieldName); + const expectedXMLFieldDetail = XMLMarkupMockService.getNumberXMLFieldDetail(); - const validXMLFileExtensionName = 'Checkbox__c.field-meta.xml'; - const expectedVSCodeFileTypeEnum = 1; - const isXMLFileType:boolean = XmlFileProcessor.isXMLFileType(validXMLFileExtensionName, expectedVSCodeFileTypeEnum); - expect(isXMLFileType).toBeTruthy(); + expect(actualFieldDetail).toEqual(expectedXMLFieldDetail); + expect(actualFieldDetail.precision).toBe(18); + expect(actualFieldDetail.scale).toBe(0); }); - test('given expected INVALID xml file extension and valid filetype enum, returns true', () => { + test('given currency field with precision and scale XML tags, parses properties correctly', async () => { + + const xmlFieldMarkup = XMLMarkupMockService.getCurrencyFieldTypeXMLMarkup(); + const fakeFieldName = 'Currency__c.field-meta.xml'; + const actualFieldDetail = await XmlFileProcessor.processXmlFieldContent(xmlFieldMarkup, fakeFieldName); + const expectedXMLFieldDetail = XMLMarkupMockService.getCurrencyFieldDetail(); + + expect(actualFieldDetail).toEqual(expectedXMLFieldDetail); + expect(actualFieldDetail.precision).toBe(18); + expect(actualFieldDetail.scale).toBe(2); - const invalidXMLFileExtensionName = 'noxmlextensionhere.notme'; - const expectedVSCodeFileTypeEnum = 1; - const isXMLFileType:boolean = XmlFileProcessor.isXMLFileType(invalidXMLFileExtensionName, expectedVSCodeFileTypeEnum); - expect(isXMLFileType).toBeFalsy(); - }); - test('given expected valid xml file extension and INVALID filetype enum, returns true', () => { + test('given percent field with precision and scale XML tags, parses properties correctly', async () => { + + const xmlFieldMarkup = XMLMarkupMockService.getPercentFieldTypeXMLMarkup(); + const fakeFieldName = 'Percent__c.field-meta.xml'; + const actualFieldDetail = await XmlFileProcessor.processXmlFieldContent(xmlFieldMarkup, fakeFieldName); + const expectedXMLFieldDetail = XMLMarkupMockService.getPercentXMLFieldDetail(); + + expect(actualFieldDetail).toEqual(expectedXMLFieldDetail); + expect(actualFieldDetail.precision).toBe(5); + expect(actualFieldDetail.scale).toBe(2); - const validXMLFileExtensionName = 'Checkbox__c.field-meta.xml'; - const directoryTypeEnum = 2; - const isXMLFileType:boolean = XmlFileProcessor.isXMLFileType(validXMLFileExtensionName, directoryTypeEnum); - expect(isXMLFileType).toBeFalsy(); - }); - test('given expected INVALID xml file extension and INVALID filetype enum, returns true', () => { + test('given field without precision/scale/length XML tags, does not set properties', async () => { + + const xmlFieldMarkup = XMLMarkupMockService.getEmailFieldTypeXMLMarkup(); + const fakeFieldName = 'Email__c.field-meta.xml'; + const actualFieldDetail = await XmlFileProcessor.processXmlFieldContent(xmlFieldMarkup, fakeFieldName); + const expectedXMLFieldDetail = XMLMarkupMockService.getEmailXMLFieldDetail(); + + expect(actualFieldDetail).toEqual(expectedXMLFieldDetail); + expect(actualFieldDetail.precision).toBeUndefined(); + expect(actualFieldDetail.scale).toBeUndefined(); + expect(actualFieldDetail.length).toBeUndefined(); + + }); + + test('given long textarea field with length XML tag, parses length property correctly', async () => { + + const xmlFieldMarkup = XMLMarkupMockService.getLongTextAreaFieldTypeXMLMarkup(); + const fakeFieldName = 'LongTextArea__c.field-meta.xml'; + const actualFieldDetail = await XmlFileProcessor.processXmlFieldContent(xmlFieldMarkup, fakeFieldName); + const expectedXMLFieldDetail = XMLMarkupMockService.getLongTextAreaXMLFieldDetail(); + + expect(actualFieldDetail).toEqual(expectedXMLFieldDetail); + expect(actualFieldDetail.length).toBe(131072); + + }); + + test('given rich text area field with length XML tag, parses length property correctly', async () => { + + const xmlFieldMarkup = XMLMarkupMockService.getRichTextAreaFieldTypeXMLMarkup(); + const fakeFieldName = 'RichTextArea__c.field-meta.xml'; + const actualFieldDetail = await XmlFileProcessor.processXmlFieldContent(xmlFieldMarkup, fakeFieldName); + const expectedXMLFieldDetail = XMLMarkupMockService.getRichTextAreaXMLFieldDetail(); + + expect(actualFieldDetail).toEqual(expectedXMLFieldDetail); + expect(actualFieldDetail.length).toBe(32768); - const invalidXMLFileExtensionName = 'noxmlextensionhere.notme'; - const directoryTypeEnum = 2; - const isXMLFileType:boolean = XmlFileProcessor.isXMLFileType(invalidXMLFileExtensionName, directoryTypeEnum); - expect(isXMLFileType).toBeFalsy(); - }); }); \ No newline at end of file diff --git a/src/treecipe/src/XMLProcessingService/tests/mocks/XMLMarkupMockService.ts b/src/treecipe/src/XMLProcessingService/tests/mocks/XMLMarkupMockService.ts index b554410..126dac2 100644 --- a/src/treecipe/src/XMLProcessingService/tests/mocks/XMLMarkupMockService.ts +++ b/src/treecipe/src/XMLProcessingService/tests/mocks/XMLMarkupMockService.ts @@ -102,6 +102,7 @@ export class XMLMarkupMockService { fieldType: "Html", apiName: "TextAreaRich__c", fieldLabel: "TextAreaRich", + length: 32768, xmlMarkup: this.getRichTextAreaFieldTypeXMLMarkup() }; return richTextAreaXMLField; @@ -130,6 +131,7 @@ export class XMLMarkupMockService { const longTextAreaXMLField: XMLFieldDetail = { fieldType: "LongTextArea", apiName: "Text_Area_Long__c", + length: 131072, fieldLabel: "Text Area Long", xmlMarkup: this.getLongTextAreaFieldTypeXMLMarkup() }; @@ -190,10 +192,12 @@ export class XMLMarkupMockService { fieldType: "Number", apiName: "Number__c", fieldLabel: "Number", + precision: 18, + scale: 0, xmlMarkup: this.getNumberFieldTypeXMLMarkup() }; - return numberXMLField; - + return numberXMLField; + } static getNumberFieldTypeXMLMarkup() { @@ -233,7 +237,6 @@ export class XMLMarkupMockService { false false - 2 false Location @@ -263,9 +266,7 @@ export class XMLMarkupMockService { $Organization.Longitude BlankAsZero - 18 false - 2 false Number false @@ -995,6 +996,8 @@ export class XMLMarkupMockService { fieldType: "Currency", apiName: "Currency__c", fieldLabel: "Currency", + precision: 18, + scale: 2, xmlMarkup: this.getCurrencyFieldTypeXMLMarkup() }; @@ -1002,6 +1005,40 @@ export class XMLMarkupMockService { } + static getPercentXMLFieldDetail() { + + let xmlFieldDetail: XMLFieldDetail = { + fieldType: "Percent", + apiName: "Percent__c", + fieldLabel: "Percent", + precision: 5, + scale: 2, + xmlMarkup: this.getPercentFieldTypeXMLMarkup() + }; + + return xmlFieldDetail; + + } + + static getPercentFieldTypeXMLMarkup() { + + const percentXMLMarkup = ` + + + Percent__c + + 5 + false + 2 + false + Percent + +`; + + return percentXMLMarkup; + + } + static getCurrencyFieldTypeXMLMarkup() { const currencyXMLMarkup = ` @@ -1514,6 +1551,7 @@ export class XMLMarkupMockService { fieldType: "Text", apiName: "Text__c", fieldLabel: "Text", + length: 255, xmlMarkup: this.getTextFieldTypeXMLMarkup() }; From 990d2e1172021ad6ba050f96bc640fc2ebc6665c Mon Sep 17 00:00:00 2001 From: jdschleicher Date: Mon, 17 Nov 2025 20:47:40 -0500 Subject: [PATCH 04/10] 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. --- .../FakerJSRecipeFakerService.ts | 28 +++++++++---------- .../IRecipeFakerService.ts | 1 + .../SnowfakeryRecipeFakerService.ts | 20 ++++++------- .../src/RecipeService/RecipeService.ts | 9 +++++- .../tests/FakerJSRecipeService.test.ts | 4 +-- .../RecipeService/tests/RecipeService.test.ts | 4 +-- 6 files changed, 36 insertions(+), 30 deletions(-) diff --git a/src/treecipe/src/RecipeFakerService.ts/FakerJSRecipeFakerService/FakerJSRecipeFakerService.ts b/src/treecipe/src/RecipeFakerService.ts/FakerJSRecipeFakerService/FakerJSRecipeFakerService.ts index 9f1f6a9..40a701f 100644 --- a/src/treecipe/src/RecipeFakerService.ts/FakerJSRecipeFakerService/FakerJSRecipeFakerService.ts +++ b/src/treecipe/src/RecipeFakerService.ts/FakerJSRecipeFakerService/FakerJSRecipeFakerService.ts @@ -513,25 +513,25 @@ ${this.generateTabs(5)}${randomChoicesBreakdown}`; } buildNumericRecipeValueWithPrecisionAndScale(precision: number, scale?: number): string { - // Handle all numeric fields (number, currency, percent) with precision and optional scale + const effectiveScale = scale ?? 0; + const maxValueByPrecision = '9'.repeat(precision); if (effectiveScale === 0) { - // Integer fields (number with scale=0, or scale not specified) - // Cap precision at 15 to get accurate all-9's result (JavaScript can't represent larger integers) - const safePrecision = Math.min(precision, 18); - const maxValueString = '9'.repeat(safePrecision); - const maxValue = parseInt(maxValueString, 10); - return `${this.openingRecipeSyntax} faker.number.int({min: 0, max: ${maxValue}}) ${this.closingRecipeSyntax}`; + return `${this.openingRecipeSyntax} faker.number.int({min: 0, max: ${maxValueByPrecision}}) ${this.closingRecipeSyntax}`; } else { - // Decimal fields (currency, percent with scale > 0) - // For decimal fields, precision includes decimal places, so max integer part is precision - scale - const integerPrecision = precision - effectiveScale; - const safeIntegerPrecision = Math.min(integerPrecision, 18); - const maxValueString = '9'.repeat(safeIntegerPrecision); - const maxValue = parseInt(maxValueString, 10); - return `${this.openingRecipeSyntax} faker.finance.amount({min: 0, max: ${maxValue}, dec: ${effectiveScale}}) ${this.closingRecipeSyntax}`; + return `${this.openingRecipeSyntax} faker.finance.amount({min: 0, max: ${maxValueByPrecision}, 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 maxValueByPrecision = '9'.repeat(precision); + + return `${this.openingRecipeSyntax} faker.finance.amount({min: 0, max: ${maxValueByPrecision}, dec: ${effectiveScale}}) ${this.closingRecipeSyntax}`; + } } diff --git a/src/treecipe/src/RecipeFakerService.ts/IRecipeFakerService.ts b/src/treecipe/src/RecipeFakerService.ts/IRecipeFakerService.ts index 48085fb..375577c 100644 --- a/src/treecipe/src/RecipeFakerService.ts/IRecipeFakerService.ts +++ b/src/treecipe/src/RecipeFakerService.ts/IRecipeFakerService.ts @@ -30,4 +30,5 @@ export interface IRecipeFakerService { getStandardAndGlobalValueSetTODOPlaceholderWithExample(): string buildTextRecipeValueWithLength(length: number): string buildNumericRecipeValueWithPrecisionAndScale(precision: number, scale?: number): string + buildCurrencyRecipeValueWithPrecisionAndScale(precision: number, scale?: number): string } diff --git a/src/treecipe/src/RecipeFakerService.ts/SnowfakeryRecipeFakerService/SnowfakeryRecipeFakerService.ts b/src/treecipe/src/RecipeFakerService.ts/SnowfakeryRecipeFakerService/SnowfakeryRecipeFakerService.ts index 114a212..3eb4ada 100644 --- a/src/treecipe/src/RecipeFakerService.ts/SnowfakeryRecipeFakerService/SnowfakeryRecipeFakerService.ts +++ b/src/treecipe/src/RecipeFakerService.ts/SnowfakeryRecipeFakerService/SnowfakeryRecipeFakerService.ts @@ -407,21 +407,19 @@ ${this.generateTabs(5)}${randomChoicesBreakdown}`; buildNumericRecipeValueWithPrecisionAndScale(precision: number, scale?: number): string { // Handle all numeric fields (number, currency, percent) with precision and optional scale const effectiveScale = scale ?? 0; + const maxValuePrecision = '9'.repeat(precision); if (effectiveScale === 0) { - // Integer fields (number with scale=0, or scale not specified) - // Create max value by repeating '9' precision times, avoiding Math.pow precision issues - const maxValueString = '9'.repeat(precision); - const maxValue = parseInt(maxValueString, 10); - return `${this.openingRecipeSyntax}fake.random_int(min=0, max=${maxValue})${this.closingRecipeSyntax}`; + return `${this.openingRecipeSyntax}fake.random_int(min=0, max=${maxValuePrecision})${this.closingRecipeSyntax}`; } else { - // Decimal fields (currency, percent with scale > 0) - // For decimal fields, precision includes decimal places, so max integer part is precision - scale - const integerPrecision = precision - effectiveScale; - const maxValueString = '9'.repeat(integerPrecision); - const maxValue = parseInt(maxValueString, 10); - return `${this.openingRecipeSyntax}fake.pydecimal(left_digits=${integerPrecision}, right_digits=${effectiveScale}, positive=True)${this.closingRecipeSyntax}`; + return `${this.openingRecipeSyntax}fake.pydecimal(left_digits=${maxValuePrecision}, right_digits=${effectiveScale}, positive=True)${this.closingRecipeSyntax}`; } } + buildCurrencyRecipeValueWithPrecisionAndScale(precision: number, scale?: number): string { + // Special handling for currency fields - use full precision as left_digits + const effectiveScale = scale ?? 0; + return `${this.openingRecipeSyntax}fake.pydecimal(left_digits=${precision}, right_digits=${effectiveScale}, positive=True)${this.closingRecipeSyntax}`; + } + } diff --git a/src/treecipe/src/RecipeService/RecipeService.ts b/src/treecipe/src/RecipeService/RecipeService.ts index be41282..3facf15 100644 --- a/src/treecipe/src/RecipeService/RecipeService.ts +++ b/src/treecipe/src/RecipeService/RecipeService.ts @@ -121,7 +121,6 @@ export class RecipeService { // Fall through to default if no length case 'number': - case 'currency': case 'percent': if (xmlFieldDetail.precision) { @@ -130,6 +129,14 @@ export class RecipeService { } // 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); diff --git a/src/treecipe/src/RecipeService/tests/FakerJSRecipeService.test.ts b/src/treecipe/src/RecipeService/tests/FakerJSRecipeService.test.ts index 2450d3f..6855f1f 100644 --- a/src/treecipe/src/RecipeService/tests/FakerJSRecipeService.test.ts +++ b/src/treecipe/src/RecipeService/tests/FakerJSRecipeService.test.ts @@ -191,7 +191,7 @@ 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: 999999999999999}) }}`; + const expectedFakerJSExpressionForNumber = `\${{ faker.number.int({min: 0, max: 999999999999999999}) }}`; const recordTypeNameByRecordTypeNameToXMLMarkup = {}; const actualFakerJSForNumber = recipeServiceWithFakerJS.getRecipeFakeValueByXMLFieldDetail(expectedXMLDetailForNumber, recordTypeNameByRecordTypeNameToXMLMarkup); @@ -202,7 +202,7 @@ 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({min: 0, max: 999999999999999, dec: 2}) }}"; + const expectedFakerJSExpressionForCurrency = "\${{ faker.finance.amount({min: 0, max: 999999999999999999, dec: 2}) }}"; const recordTypeNameByRecordTypeNameToXMLMarkup = {}; const actualFakerJSForCurrency = recipeServiceWithFakerJS.getRecipeFakeValueByXMLFieldDetail(expectedXMLDetailForCurrency, recordTypeNameByRecordTypeNameToXMLMarkup); diff --git a/src/treecipe/src/RecipeService/tests/RecipeService.test.ts b/src/treecipe/src/RecipeService/tests/RecipeService.test.ts index a1ac717..7b91315 100644 --- a/src/treecipe/src/RecipeService/tests/RecipeService.test.ts +++ b/src/treecipe/src/RecipeService/tests/RecipeService.test.ts @@ -124,7 +124,7 @@ 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=999999999999999)}}'; + const expectedSnowfakeryValueForNumber = '${{fake.random_int(min=0, max=999999999999999999)}}'; const recordTypeNameByRecordTypeNameToXMLMarkup = {}; const actualSnowfakeryValueForNumber = recipeServiceWithSnow.getRecipeFakeValueByXMLFieldDetail(expectedXMLDetailForNumber, recordTypeNameByRecordTypeNameToXMLMarkup); @@ -136,7 +136,7 @@ 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=16, right_digits=2, positive=True)}}'; + const expectedSnowfakeryValueForCurrency = '${{fake.pydecimal(left_digits=18, right_digits=2, positive=True)}}'; const recordTypeNameByRecordTypeNameToXMLMarkup = {}; const actualSnowfakeryValueForCurrency = recipeServiceWithSnow.getRecipeFakeValueByXMLFieldDetail(expectedXMLDetailForCurrency, recordTypeNameByRecordTypeNameToXMLMarkup); From 15d021e24fecf8cc2563ce3e6046450e07d52e71 Mon Sep 17 00:00:00 2001 From: jdschleicher Date: Mon, 17 Nov 2025 21:11:04 -0500 Subject: [PATCH 05/10] 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. --- .../FakerJSRecipeFakerService.ts | 12 ++++++++---- .../RecipeService/tests/FakerJSRecipeService.test.ts | 6 ++++-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/treecipe/src/RecipeFakerService.ts/FakerJSRecipeFakerService/FakerJSRecipeFakerService.ts b/src/treecipe/src/RecipeFakerService.ts/FakerJSRecipeFakerService/FakerJSRecipeFakerService.ts index 40a701f..c73c2c5 100644 --- a/src/treecipe/src/RecipeFakerService.ts/FakerJSRecipeFakerService/FakerJSRecipeFakerService.ts +++ b/src/treecipe/src/RecipeFakerService.ts/FakerJSRecipeFakerService/FakerJSRecipeFakerService.ts @@ -518,19 +518,23 @@ ${this.generateTabs(5)}${randomChoicesBreakdown}`; const maxValueByPrecision = '9'.repeat(precision); if (effectiveScale === 0) { - return `${this.openingRecipeSyntax} faker.number.int({min: 0, max: ${maxValueByPrecision}}) ${this.closingRecipeSyntax}`; + return `| + ${this.openingRecipeSyntax} faker.number.int({min: 0, max: ${maxValueByPrecision}}) ${this.closingRecipeSyntax}`; } else { - return `${this.openingRecipeSyntax} faker.finance.amount({min: 0, max: ${maxValueByPrecision}, dec: ${effectiveScale}}) ${this.closingRecipeSyntax}`; + return `| + ${this.openingRecipeSyntax} faker.finance.amount({min: 0, max: ${maxValueByPrecision}, 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 maxValueByPrecision = '9'.repeat(precision); - return `${this.openingRecipeSyntax} faker.finance.amount({min: 0, max: ${maxValueByPrecision}, dec: ${effectiveScale}}) ${this.closingRecipeSyntax}`; + return `| + ${this.openingRecipeSyntax} faker.finance.amount({min: 0, max: ${maxValueByPrecision}, dec: ${effectiveScale}}) ${this.closingRecipeSyntax}`; } diff --git a/src/treecipe/src/RecipeService/tests/FakerJSRecipeService.test.ts b/src/treecipe/src/RecipeService/tests/FakerJSRecipeService.test.ts index 6855f1f..a5a90b9 100644 --- a/src/treecipe/src/RecipeService/tests/FakerJSRecipeService.test.ts +++ b/src/treecipe/src/RecipeService/tests/FakerJSRecipeService.test.ts @@ -191,7 +191,8 @@ 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: 999999999999999999}) }}`; + const expectedFakerJSExpressionForNumber = `| + \${{ faker.number.int({min: 0, max: 999999999999999999}) }}`; const recordTypeNameByRecordTypeNameToXMLMarkup = {}; const actualFakerJSForNumber = recipeServiceWithFakerJS.getRecipeFakeValueByXMLFieldDetail(expectedXMLDetailForNumber, recordTypeNameByRecordTypeNameToXMLMarkup); @@ -202,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({min: 0, max: 999999999999999999, dec: 2}) }}"; + const expectedFakerJSExpressionForCurrency = `| + \${{ faker.finance.amount({min: 0, max: 999999999999999999, dec: 2}) }}`; const recordTypeNameByRecordTypeNameToXMLMarkup = {}; const actualFakerJSForCurrency = recipeServiceWithFakerJS.getRecipeFakeValueByXMLFieldDetail(expectedXMLDetailForCurrency, recordTypeNameByRecordTypeNameToXMLMarkup); From 303b8746832a0dc6386aa010ad5a4311684d6ddb Mon Sep 17 00:00:00 2001 From: jdschleicher Date: Mon, 24 Nov 2025 04:56:39 -0500 Subject: [PATCH 06/10] 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. --- .../FakerJSRecipeFakerService.ts | 16 ++++++++-------- .../tests/FakerJSRecipeService.test.ts | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/treecipe/src/RecipeFakerService.ts/FakerJSRecipeFakerService/FakerJSRecipeFakerService.ts b/src/treecipe/src/RecipeFakerService.ts/FakerJSRecipeFakerService/FakerJSRecipeFakerService.ts index c73c2c5..275786a 100644 --- a/src/treecipe/src/RecipeFakerService.ts/FakerJSRecipeFakerService/FakerJSRecipeFakerService.ts +++ b/src/treecipe/src/RecipeFakerService.ts/FakerJSRecipeFakerService/FakerJSRecipeFakerService.ts @@ -515,27 +515,27 @@ ${this.generateTabs(5)}${randomChoicesBreakdown}`; buildNumericRecipeValueWithPrecisionAndScale(precision: number, scale?: number): string { const effectiveScale = scale ?? 0; - const maxValueByPrecision = '9'.repeat(precision); + const maxNumbersLeftOfDecimal = '9'.repeat(precision - effectiveScale); if (effectiveScale === 0) { return `| - ${this.openingRecipeSyntax} faker.number.int({min: 0, max: ${maxValueByPrecision}}) ${this.closingRecipeSyntax}`; + ${this.openingRecipeSyntax} faker.number.int({min: 0, max: ${maxNumbersLeftOfDecimal}}) ${this.closingRecipeSyntax}`; } else { return `| - ${this.openingRecipeSyntax} faker.finance.amount({min: 0, max: ${maxValueByPrecision}, dec: ${effectiveScale}}) ${this.closingRecipeSyntax}`; + ${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 maxValueByPrecision = '9'.repeat(precision); + const maxNumbersLeftOfDecimal = '9'.repeat(precision - effectiveScale); return `| - ${this.openingRecipeSyntax} faker.finance.amount({min: 0, max: ${maxValueByPrecision}, dec: ${effectiveScale}}) ${this.closingRecipeSyntax}`; - + ${this.openingRecipeSyntax} faker.finance.amount({min: 0, max: ${maxNumbersLeftOfDecimal}, dec: ${effectiveScale}}) ${this.closingRecipeSyntax}`; + } } diff --git a/src/treecipe/src/RecipeService/tests/FakerJSRecipeService.test.ts b/src/treecipe/src/RecipeService/tests/FakerJSRecipeService.test.ts index a5a90b9..92cc12d 100644 --- a/src/treecipe/src/RecipeService/tests/FakerJSRecipeService.test.ts +++ b/src/treecipe/src/RecipeService/tests/FakerJSRecipeService.test.ts @@ -204,7 +204,7 @@ describe('FakerJSRecipeService IRecipeService Implementation Shared Intstance Te const expectedXMLDetailForCurrency:XMLFieldDetail = XMLMarkupMockService.getCurrencyFieldDetail(); const expectedFakerJSExpressionForCurrency = `| - \${{ faker.finance.amount({min: 0, max: 999999999999999999, dec: 2}) }}`; + \${{ faker.finance.amount({min: 0, max: 9999999999999999, dec: 2}) }}`; const recordTypeNameByRecordTypeNameToXMLMarkup = {}; const actualFakerJSForCurrency = recipeServiceWithFakerJS.getRecipeFakeValueByXMLFieldDetail(expectedXMLDetailForCurrency, recordTypeNameByRecordTypeNameToXMLMarkup); From 4d0e5c9c07665b3cff58e3fb4672114e7602b6d5 Mon Sep 17 00:00:00 2001 From: jdschleicher Date: Mon, 24 Nov 2025 04:59:10 -0500 Subject: [PATCH 07/10] 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. --- .../SnowfakeryRecipeFakerService.ts | 9 +++++---- .../src/RecipeService/tests/RecipeService.test.ts | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/treecipe/src/RecipeFakerService.ts/SnowfakeryRecipeFakerService/SnowfakeryRecipeFakerService.ts b/src/treecipe/src/RecipeFakerService.ts/SnowfakeryRecipeFakerService/SnowfakeryRecipeFakerService.ts index 3eb4ada..811279a 100644 --- a/src/treecipe/src/RecipeFakerService.ts/SnowfakeryRecipeFakerService/SnowfakeryRecipeFakerService.ts +++ b/src/treecipe/src/RecipeFakerService.ts/SnowfakeryRecipeFakerService/SnowfakeryRecipeFakerService.ts @@ -407,19 +407,20 @@ ${this.generateTabs(5)}${randomChoicesBreakdown}`; buildNumericRecipeValueWithPrecisionAndScale(precision: number, scale?: number): string { // Handle all numeric fields (number, currency, percent) with precision and optional scale const effectiveScale = scale ?? 0; - const maxValuePrecision = '9'.repeat(precision); + const maxNumbersLeftOfDecimal = '9'.repeat(precision - effectiveScale); if (effectiveScale === 0) { - return `${this.openingRecipeSyntax}fake.random_int(min=0, max=${maxValuePrecision})${this.closingRecipeSyntax}`; + return `${this.openingRecipeSyntax}fake.random_int(min=0, max=${maxNumbersLeftOfDecimal})${this.closingRecipeSyntax}`; } else { - return `${this.openingRecipeSyntax}fake.pydecimal(left_digits=${maxValuePrecision}, right_digits=${effectiveScale}, positive=True)${this.closingRecipeSyntax}`; + return `${this.openingRecipeSyntax}fake.pydecimal(left_digits=${maxNumbersLeftOfDecimal}, right_digits=${effectiveScale}, positive=True)${this.closingRecipeSyntax}`; } } buildCurrencyRecipeValueWithPrecisionAndScale(precision: number, scale?: number): string { // Special handling for currency fields - use full precision as left_digits const effectiveScale = scale ?? 0; - return `${this.openingRecipeSyntax}fake.pydecimal(left_digits=${precision}, right_digits=${effectiveScale}, positive=True)${this.closingRecipeSyntax}`; + const maxNumbersLeftOfDecimal = precision - effectiveScale; + return `${this.openingRecipeSyntax}fake.pydecimal(left_digits=${maxNumbersLeftOfDecimal}, right_digits=${effectiveScale}, positive=True)${this.closingRecipeSyntax}`; } } diff --git a/src/treecipe/src/RecipeService/tests/RecipeService.test.ts b/src/treecipe/src/RecipeService/tests/RecipeService.test.ts index 7b91315..97e43e7 100644 --- a/src/treecipe/src/RecipeService/tests/RecipeService.test.ts +++ b/src/treecipe/src/RecipeService/tests/RecipeService.test.ts @@ -136,7 +136,7 @@ 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=18, 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); From cb55a0c4c6898738b4d426069863b45fec4793ac Mon Sep 17 00:00:00 2001 From: jdschleicher Date: Mon, 24 Nov 2025 05:00:57 -0500 Subject: [PATCH 08/10] 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. --- .../SnowfakeryRecipeFakerService.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/treecipe/src/RecipeFakerService.ts/SnowfakeryRecipeFakerService/SnowfakeryRecipeFakerService.ts b/src/treecipe/src/RecipeFakerService.ts/SnowfakeryRecipeFakerService/SnowfakeryRecipeFakerService.ts index 811279a..2d210ad 100644 --- a/src/treecipe/src/RecipeFakerService.ts/SnowfakeryRecipeFakerService/SnowfakeryRecipeFakerService.ts +++ b/src/treecipe/src/RecipeFakerService.ts/SnowfakeryRecipeFakerService/SnowfakeryRecipeFakerService.ts @@ -405,7 +405,7 @@ ${this.generateTabs(5)}${randomChoicesBreakdown}`; } buildNumericRecipeValueWithPrecisionAndScale(precision: number, scale?: number): string { - // Handle all numeric fields (number, currency, percent) with precision and optional scale + const effectiveScale = scale ?? 0; const maxNumbersLeftOfDecimal = '9'.repeat(precision - effectiveScale); @@ -414,13 +414,15 @@ ${this.generateTabs(5)}${randomChoicesBreakdown}`; } else { return `${this.openingRecipeSyntax}fake.pydecimal(left_digits=${maxNumbersLeftOfDecimal}, right_digits=${effectiveScale}, positive=True)${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 = precision - effectiveScale; return `${this.openingRecipeSyntax}fake.pydecimal(left_digits=${maxNumbersLeftOfDecimal}, right_digits=${effectiveScale}, positive=True)${this.closingRecipeSyntax}`; + } } From 30889787a0ffb2a41a19db2eb196ba5269493970 Mon Sep 17 00:00:00 2001 From: jdschleicher Date: Mon, 24 Nov 2025 05:23:11 -0500 Subject: [PATCH 09/10] 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 --- CHANGELOG.md | 65 ++++++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 2 +- 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 33a0fac..4ae2344 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,70 @@ # 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. **Numeric Field Scale Support** +Added comprehensive support for scale in numeric recipe value generation, ensuring accurate data generation for currency and number fields with fractional precision. + +#### 2. **YAML Block Scalar Formatting** +Format faker expressions as YAML block scalars in recipes, improving readability and maintainability of complex multi-line expressions. + +#### 3. **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. + +### 🐛 Bug Fixes + +#### 1. **Max Value Calculation Correction** +Fixed incorrect max value calculations for numeric and currency fields to prevent overflow and ensure data integrity. + +#### 2. **Precision-Based Left Digits Calculation** +Corrected the calculation of left_digits for numeric and currency fields based on precision - scale, fixing value range generation. + +#### 3. **JavaScript Number Overflow Prevention** +Capped numeric precision at 15 to avoid JavaScript number overflow issues when processing large precision values. + +#### 4. **Code Cleanup** +Fixed missing closing braces and removed unnecessary comments in `SnowfakeryRecipeFakerService`. + +### 🔧 Technical Details + +**Files Modified**: +- `src/treecipe/src/RecipeFakerService.ts/SnowfakeryRecipeFakerService/SnowfakeryRecipeFakerService.ts` +- Additional related files in RecipeFakerService for numeric handling + +**Code Example - Currency Field XML to FakerJS YAML**: + +Given a custom object field XML markup with precision and scale: + +```xml + + Price__c + Product price + false + + 8 + false + 2 + Currency + false + +``` + +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 diff --git a/package.json b/package.json index 90cce0b..17b42cb 100644 --- a/package.json +++ b/package.json @@ -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" }, From 9225d208befe3281e8ce9f2fbdee455021ebf434 Mon Sep 17 00:00:00 2001 From: jdschleicher Date: Mon, 24 Nov 2025 05:25:01 -0500 Subject: [PATCH 10/10] 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. --- CHANGELOG.md | 26 +------------------------- 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ae2344..d50f5d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,35 +4,11 @@ ### 🎯 Major Features -#### 1. **Numeric Field Scale Support** -Added comprehensive support for scale in numeric recipe value generation, ensuring accurate data generation for currency and number fields with fractional precision. - -#### 2. **YAML Block Scalar Formatting** -Format faker expressions as YAML block scalars in recipes, improving readability and maintainability of complex multi-line expressions. - -#### 3. **Text and Numeric Value Constraints** +#### 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. -### 🐛 Bug Fixes - -#### 1. **Max Value Calculation Correction** -Fixed incorrect max value calculations for numeric and currency fields to prevent overflow and ensure data integrity. - -#### 2. **Precision-Based Left Digits Calculation** -Corrected the calculation of left_digits for numeric and currency fields based on precision - scale, fixing value range generation. - -#### 3. **JavaScript Number Overflow Prevention** -Capped numeric precision at 15 to avoid JavaScript number overflow issues when processing large precision values. - -#### 4. **Code Cleanup** -Fixed missing closing braces and removed unnecessary comments in `SnowfakeryRecipeFakerService`. - ### 🔧 Technical Details -**Files Modified**: -- `src/treecipe/src/RecipeFakerService.ts/SnowfakeryRecipeFakerService/SnowfakeryRecipeFakerService.ts` -- Additional related files in RecipeFakerService for numeric handling - **Code Example - Currency Field XML to FakerJS YAML**: Given a custom object field XML markup with precision and scale: