From a558ea47bafcea8784fbfb952df487faaadfd6d6 Mon Sep 17 00:00:00 2001 From: Nataly Carbonell Date: Thu, 18 Sep 2025 00:59:28 -0400 Subject: [PATCH 01/40] feat(compass-collection): LLM Output Validation - Mock Data Generator CLOUDP-333855 --- .../mock-data-generator-modal.spec.tsx | 188 ++++++++++++++++-- .../src/modules/collection-tab.ts | 99 ++++++--- 2 files changed, 243 insertions(+), 44 deletions(-) diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.spec.tsx b/packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.spec.tsx index 1b651c2f2b6..5c82204ade8 100644 --- a/packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.spec.tsx +++ b/packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.spec.tsx @@ -16,6 +16,20 @@ import type { CollectionState } from '../../modules/collection-tab'; import { default as collectionTabReducer } from '../../modules/collection-tab'; import type { ConnectionInfo } from '@mongodb-js/connection-info'; import type { MockDataSchemaResponse } from '@mongodb-js/compass-generative-ai'; +import type { SchemaAnalysisState } from '../../schema-analysis-types'; + +const defaultSchemaAnalysisState: SchemaAnalysisState = { + status: 'complete', + processedSchema: { + name: { + type: 'String', + probability: 1.0, + sample_values: ['John', 'Jane'], + }, + }, + sampleDocument: { name: 'John' }, + schemaMetadata: { maxNestingDepth: 1, validationRules: null }, +}; describe('MockDataGeneratorModal', () => { async function renderModal({ @@ -23,6 +37,7 @@ describe('MockDataGeneratorModal', () => { currentStep = MockDataGeneratorStep.SCHEMA_CONFIRMATION, enableGenAISampleDocumentPassing = false, mockServices = createMockServices(), + schemaAnalysis = defaultSchemaAnalysisState, connectionInfo, }: { isOpen?: boolean; @@ -30,23 +45,13 @@ describe('MockDataGeneratorModal', () => { currentStep?: MockDataGeneratorStep; mockServices?: any; connectionInfo?: ConnectionInfo; + schemaAnalysis?: SchemaAnalysisState; } = {}) { const initialState: CollectionState = { workspaceTabId: 'test-workspace-tab-id', namespace: 'test.collection', metadata: null, - schemaAnalysis: { - status: 'complete', - processedSchema: { - name: { - type: 'String', - probability: 1.0, - sample_values: ['John', 'Jane'], - }, - }, - sampleDocument: { name: 'John' }, - schemaMetadata: { maxNestingDepth: 1, validationRules: null }, - }, + schemaAnalysis, fakerSchemaGeneration: { status: 'idle', }, @@ -280,6 +285,33 @@ describe('MockDataGeneratorModal', () => { }); describe('on the schema editor step', () => { + const mockSchemaAnalysis: SchemaAnalysisState = { + ...defaultSchemaAnalysisState, + processedSchema: { + name: { + type: 'String', + probability: 1.0, + }, + age: { + type: 'Int32', + probability: 1.0, + }, + email: { + type: 'String', + probability: 1.0, + }, + username: { + type: 'String', + probability: 1.0, + }, + }, + sampleDocument: { + name: 'Jane', + age: 99, + email: 'Jane@email.com', + username: 'JaneDoe123', + }, + }; const mockServicesWithMockDataResponse = createMockServices(); mockServicesWithMockDataResponse.atlasAiService.getMockDataSchema = () => Promise.resolve({ @@ -320,11 +352,11 @@ describe('MockDataGeneratorModal', () => { resolve({ fields: [], }), - 1000 + 1 ) ); - await renderModal(); + await renderModal({ mockServices }); // advance to the schema editor step userEvent.click(screen.getByText('Confirm')); @@ -332,7 +364,10 @@ describe('MockDataGeneratorModal', () => { }); it('shows the faker schema editor when the faker schema generation is completed', async () => { - await renderModal({ mockServices: mockServicesWithMockDataResponse }); + await renderModal({ + mockServices: mockServicesWithMockDataResponse, + schemaAnalysis: mockSchemaAnalysis, + }); // advance to the schema editor step userEvent.click(screen.getByText('Confirm')); @@ -343,7 +378,10 @@ describe('MockDataGeneratorModal', () => { }); it('shows correct values for the faker schema editor', async () => { - await renderModal({ mockServices: mockServicesWithMockDataResponse }); + await renderModal({ + mockServices: mockServicesWithMockDataResponse, + schemaAnalysis: mockSchemaAnalysis, + }); // advance to the schema editor step userEvent.click(screen.getByText('Confirm')); @@ -386,6 +424,124 @@ describe('MockDataGeneratorModal', () => { ); }); + it('does not show any fields that are not in the input schema', async () => { + const mockServices = createMockServices(); + mockServices.atlasAiService.getMockDataSchema = () => + Promise.resolve({ + content: { + fields: [ + { + fieldPath: 'name', + mongoType: 'string', + fakerMethod: 'person.firstName', + fakerArgs: [], + isArray: false, + probability: 1.0, + }, + { + fieldPath: 'email', + mongoType: 'string', + fakerMethod: 'internet.email', + fakerArgs: [], + isArray: false, + probability: 1.0, + }, + ], + }, + }); + await renderModal({ + mockServices, + }); + + // advance to the schema editor step + userEvent.click(screen.getByText('Confirm')); + + await waitFor(() => { + expect(screen.getByTestId('faker-schema-editor')).to.exist; + }); + + expect(screen.getByText('name')).to.exist; + expect(screen.queryByText('email')).to.not.exist; + }); + + it('shows unmapped fields as "Unrecognized"', async () => { + const mockServices = createMockServices(); + mockServices.atlasAiService.getMockDataSchema = () => + Promise.resolve({ + content: { + fields: [ + { + fieldPath: 'name', + mongoType: 'String', + fakerMethod: 'person.firstName', + fakerArgs: [], + isArray: false, + probability: 1.0, + }, + { + fieldPath: 'age', + mongoType: 'Int32', + fakerMethod: 'number.int', + fakerArgs: [], + isArray: false, + probability: 1.0, + }, + ], + }, + }); + + await renderModal({ + mockServices, + schemaAnalysis: { + ...defaultSchemaAnalysisState, + processedSchema: { + name: { + type: 'String', + probability: 1.0, + }, + age: { + type: 'Int32', + probability: 1.0, + }, + type: { + type: 'String', + probability: 1.0, + sample_values: ['cat', 'dog'], + }, + }, + sampleDocument: { name: 'Peaches', age: 10, type: 'cat' }, + }, + }); + + // advance to the schema editor step + userEvent.click(screen.getByText('Confirm')); + + await waitFor(() => { + expect(screen.getByTestId('faker-schema-editor')).to.exist; + }); + + // select the "name" field + userEvent.click(screen.getByText('name')); + expect(screen.getByLabelText('JSON Type')).to.have.value('String'); + expect(screen.getByLabelText('Faker Function')).to.have.value( + 'person.firstName' + ); + + // select the "age" field + userEvent.click(screen.getByText('age')); + expect(screen.getByLabelText('JSON Type')).to.have.value('Int32'); + expect(screen.getByLabelText('Faker Function')).to.have.value( + 'number.int' + ); + + // select the "type" field + userEvent.click(screen.getByText('type')); + expect(screen.getByLabelText('JSON Type')).to.have.value('String'); + expect(screen.getByLabelText('Faker Function')).to.have.value( + 'Unrecognized' + ); + }); + it('disables the Next button when the faker schema mapping is not confirmed', async () => { await renderModal({ mockServices: mockServicesWithMockDataResponse, diff --git a/packages/compass-collection/src/modules/collection-tab.ts b/packages/compass-collection/src/modules/collection-tab.ts index 74ba34dd5a1..7580c64073f 100644 --- a/packages/compass-collection/src/modules/collection-tab.ts +++ b/packages/compass-collection/src/modules/collection-tab.ts @@ -16,10 +16,7 @@ import type { AtlasAiService } from '@mongodb-js/compass-generative-ai/provider' import type { experimentationServiceLocator } from '@mongodb-js/compass-telemetry/provider'; import { type Logger, mongoLogId } from '@mongodb-js/compass-logging/provider'; import { type PreferencesAccess } from 'compass-preferences-model/provider'; -import type { - MockDataSchemaRequest, - MockDataSchemaResponse, -} from '@mongodb-js/compass-generative-ai'; +import type { MockDataSchemaRequest } from '@mongodb-js/compass-generative-ai'; import { isInternalFieldPath } from 'hadron-document'; import toNS from 'mongodb-ns'; import { @@ -698,32 +695,74 @@ export const cancelSchemaAnalysis = (): CollectionThunkAction => { }; }; +/** + * Validates a given faker schema against an input schema. + * + * - Filters out fields from the faker schema that do not exist in the input schema. + * - Validates the `fakerMethod` for each field, marking it as unrecognized if invalid. + * - Adds any unmapped input schema fields to the result with an unrecognized faker method. + * + * @param inputSchema - The schema definition for the input, mapping field names to their metadata. + * @param fakerSchema - The array of faker schema mappings to validate and map. + * @param logger - Logger instance used to log warnings for invalid faker methods. + * @returns An array of validated faker schema mappings, including all input schema fields. + */ const validateFakerSchema = ( - fakerSchema: MockDataSchemaResponse, + inputSchema: Record, + fakerSchema: FakerSchemaMapping[], logger: Logger -) => { - return fakerSchema.fields.map((field) => { - const { fakerMethod } = field; - - const [moduleName, methodName, ...rest] = fakerMethod.split('.'); - if ( - rest.length > 0 || - typeof (faker as any)[moduleName]?.[methodName] !== 'function' - ) { - logger.log.warn( - mongoLogId(1_001_000_372), - 'Collection', - 'Invalid faker method', - { fakerMethod } - ); - return { - ...field, - fakerMethod: UNRECOGNIZED_FAKER_METHOD, - }; - } +): FakerSchemaMapping[] => { + const inputSchemaFields = Object.keys(inputSchema); + const validatedFakerSchema = fakerSchema + // Drop fields that don't match the input schema structure + .filter((field) => inputSchema[field.fieldPath]) + .map((field) => { + const { fakerMethod } = field; + + // validate faker method + const methodSegments = fakerMethod.split('.'); + let methodRef: any = faker; + for (const segment of methodSegments) { + if ( + methodRef && + typeof methodRef === 'object' && + segment in methodRef + ) { + methodRef = methodRef[segment]; + } else { + methodRef = undefined; + break; + } + } + if (typeof methodRef !== 'function') { + logger.log.warn( + mongoLogId(1_001_000_372), + 'Collection', + 'Invalid faker method', + { fakerMethod } + ); + return { + ...field, + fakerMethod: UNRECOGNIZED_FAKER_METHOD, + }; + } - return field; - }); + return field; + }); + const unmappedInputFields = inputSchemaFields.filter( + (field) => + !validatedFakerSchema.find(({ fieldPath }) => fieldPath === field) + ); + // Default unmapped input fields to "Unrecognized" faker method + const unmappedFields = unmappedInputFields.map((field) => ({ + fieldPath: field, + fakerMethod: UNRECOGNIZED_FAKER_METHOD, + mongoType: inputSchema[field].type, + fakerArgs: [], + probability: 1, + })); + + return [...validatedFakerSchema, ...unmappedFields]; }; export const generateFakerMappings = (): CollectionThunkAction< @@ -792,7 +831,11 @@ export const generateFakerMappings = (): CollectionThunkAction< connectionInfoRef.current ); - const validatedFakerSchema = validateFakerSchema(response, logger); + const validatedFakerSchema = validateFakerSchema( + schemaAnalysis.processedSchema, + response.content.fields, + logger + ); fakerSchemaGenerationAbortControllerRef.current = undefined; dispatch({ From 9057d30f668100fd7157a939b501c2686156ec1d Mon Sep 17 00:00:00 2001 From: Nataly Carbonell Date: Thu, 18 Sep 2025 08:43:56 -0400 Subject: [PATCH 02/40] revert changes to faker method validation logic --- .../src/modules/collection-tab.ts | 20 +++++-------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/packages/compass-collection/src/modules/collection-tab.ts b/packages/compass-collection/src/modules/collection-tab.ts index 7580c64073f..9484bc89b36 100644 --- a/packages/compass-collection/src/modules/collection-tab.ts +++ b/packages/compass-collection/src/modules/collection-tab.ts @@ -720,21 +720,11 @@ const validateFakerSchema = ( const { fakerMethod } = field; // validate faker method - const methodSegments = fakerMethod.split('.'); - let methodRef: any = faker; - for (const segment of methodSegments) { - if ( - methodRef && - typeof methodRef === 'object' && - segment in methodRef - ) { - methodRef = methodRef[segment]; - } else { - methodRef = undefined; - break; - } - } - if (typeof methodRef !== 'function') { + const [moduleName, methodName, ...rest] = fakerMethod.split('.'); + if ( + rest.length > 0 || + typeof (faker as any)[moduleName]?.[methodName] !== 'function' + ) { logger.log.warn( mongoLogId(1_001_000_372), 'Collection', From c4485c1c106fae90a6e689d44a1a15e2896810d7 Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Thu, 18 Sep 2025 11:52:11 -0400 Subject: [PATCH 03/40] WIP --- .../mock-data-generator-modal/types.ts | 11 +- .../src/modules/collection-tab.ts | 124 ++++++++++++------ 2 files changed, 94 insertions(+), 41 deletions(-) diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/types.ts b/packages/compass-collection/src/components/mock-data-generator-modal/types.ts index 947911eabc1..1d09ccd69cb 100644 --- a/packages/compass-collection/src/components/mock-data-generator-modal/types.ts +++ b/packages/compass-collection/src/components/mock-data-generator-modal/types.ts @@ -19,7 +19,7 @@ type MockDataGeneratorInProgressState = { type MockDataGeneratorCompletedState = { status: 'completed'; - fakerSchema: FakerSchemaMapping[]; + fakerSchema: FakerSchema; requestId: string; }; @@ -36,3 +36,12 @@ export type MockDataGeneratorState = | MockDataGeneratorErrorState; export type FakerSchemaMapping = MockDataSchemaResponse['fields'][number]; + +export type FakerFieldMapping = { + mongoType: string; + fakerMethod: string; + fakerArgs: any[]; + probability: number; +}; + +export type FakerSchema = Record; diff --git a/packages/compass-collection/src/modules/collection-tab.ts b/packages/compass-collection/src/modules/collection-tab.ts index 9484bc89b36..615619bc3e4 100644 --- a/packages/compass-collection/src/modules/collection-tab.ts +++ b/packages/compass-collection/src/modules/collection-tab.ts @@ -37,6 +37,7 @@ import type { Document, MongoError } from 'mongodb'; import { MockDataGeneratorStep } from '../components/mock-data-generator-modal/types'; import type { FakerSchemaMapping, + FakerSchema, MockDataGeneratorState, } from '../components/mock-data-generator-modal/types'; @@ -181,7 +182,7 @@ export interface FakerMappingGenerationStartedAction { export interface FakerMappingGenerationCompletedAction { type: CollectionActions.FakerMappingGenerationCompleted; - fakerSchema: FakerSchemaMapping[]; + fakerSchema: FakerSchema; requestId: string; } @@ -695,64 +696,107 @@ export const cancelSchemaAnalysis = (): CollectionThunkAction => { }; }; +/** + * Transforms LLM array format to keyed object structure. + * Moves fieldPath from object property to object key. + */ +function transformFakerSchemaToObject( + fakerSchema: FakerSchemaMapping[] +): FakerSchema { + const result: FakerSchema = {}; + + for (const field of fakerSchema) { + const { fieldPath, ...fieldMapping } = field; + result[fieldPath] = fieldMapping; + } + + return result; +} + +/** + * Checks if the method exists and is callable on the faker object. + */ +function isValidFakerMethod(fakerMethod: string): boolean { + const parts = fakerMethod.split('.'); + + // Validate format: exactly module.method + if (parts.length !== 2) { + return false; + } + + const [moduleName, methodName] = parts; + + try { + const fakerModule = (faker as unknown as Record)[ + moduleName + ]; + return ( + fakerModule !== null && + fakerModule !== undefined && + typeof fakerModule === 'object' && + typeof (fakerModule as Record)[methodName] === 'function' + ); + } catch { + return false; + } +} + /** * Validates a given faker schema against an input schema. * - * - Filters out fields from the faker schema that do not exist in the input schema. - * - Validates the `fakerMethod` for each field, marking it as unrecognized if invalid. - * - Adds any unmapped input schema fields to the result with an unrecognized faker method. + * - Transforms LLM array format to keyed object structure + * - Validates the `fakerMethod` for each field, marking it as unrecognized if invalid + * - Adds any unmapped input schema fields to the result with an unrecognized faker method * * @param inputSchema - The schema definition for the input, mapping field names to their metadata. - * @param fakerSchema - The array of faker schema mappings to validate and map. + * @param fakerSchemaArray - The array of faker schema mappings from LLM to validate and map. * @param logger - Logger instance used to log warnings for invalid faker methods. - * @returns An array of validated faker schema mappings, including all input schema fields. + * @returns A keyed object of validated faker schema mappings, with one-to-one fields with input schema. */ const validateFakerSchema = ( inputSchema: Record, - fakerSchema: FakerSchemaMapping[], + fakerSchemaArray: FakerSchemaMapping[], logger: Logger -): FakerSchemaMapping[] => { - const inputSchemaFields = Object.keys(inputSchema); - const validatedFakerSchema = fakerSchema - // Drop fields that don't match the input schema structure - .filter((field) => inputSchema[field.fieldPath]) - .map((field) => { - const { fakerMethod } = field; - - // validate faker method - const [moduleName, methodName, ...rest] = fakerMethod.split('.'); - if ( - rest.length > 0 || - typeof (faker as any)[moduleName]?.[methodName] !== 'function' - ) { +): FakerSchema => { + // Transform to keyed object structure + const fakerSchemaRaw = transformFakerSchemaToObject(fakerSchemaArray); + + const result: FakerSchema = {}; + + // Process all input schema fields in a single O(n) pass + for (const fieldPath of Object.keys(inputSchema)) { + const fakerMapping = fakerSchemaRaw[fieldPath]; + + if (fakerMapping) { + // Validate the faker method + if (isValidFakerMethod(fakerMapping.fakerMethod)) { + result[fieldPath] = fakerMapping; + } else { logger.log.warn( mongoLogId(1_001_000_372), 'Collection', 'Invalid faker method', - { fakerMethod } + { fakerMethod: fakerMapping.fakerMethod } ); - return { - ...field, + result[fieldPath] = { + mongoType: fakerMapping.mongoType, fakerMethod: UNRECOGNIZED_FAKER_METHOD, + fakerArgs: fakerMapping.fakerArgs, + probability: fakerMapping.probability, }; } + } else { + // Field not mapped by LLM - add default + result[fieldPath] = { + mongoType: inputSchema[fieldPath].type, + fakerMethod: UNRECOGNIZED_FAKER_METHOD, + fakerArgs: [], + probability: 1, + }; + } + } - return field; - }); - const unmappedInputFields = inputSchemaFields.filter( - (field) => - !validatedFakerSchema.find(({ fieldPath }) => fieldPath === field) - ); - // Default unmapped input fields to "Unrecognized" faker method - const unmappedFields = unmappedInputFields.map((field) => ({ - fieldPath: field, - fakerMethod: UNRECOGNIZED_FAKER_METHOD, - mongoType: inputSchema[field].type, - fakerArgs: [], - probability: 1, - })); - - return [...validatedFakerSchema, ...unmappedFields]; + return result; }; export const generateFakerMappings = (): CollectionThunkAction< From 193161f40f92ba36d230a4f8d876fce0f70a517d Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Thu, 18 Sep 2025 12:03:03 -0400 Subject: [PATCH 04/40] Update schema editor screen --- .../faker-schema-editor-screen.tsx | 64 +++++++++---------- 1 file changed, 29 insertions(+), 35 deletions(-) diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/faker-schema-editor-screen.tsx b/packages/compass-collection/src/components/mock-data-generator-modal/faker-schema-editor-screen.tsx index 9629d0a251e..fb6f6ebef50 100644 --- a/packages/compass-collection/src/components/mock-data-generator-modal/faker-schema-editor-screen.tsx +++ b/packages/compass-collection/src/components/mock-data-generator-modal/faker-schema-editor-screen.tsx @@ -12,7 +12,7 @@ import { import React from 'react'; import FieldSelector from './schema-field-selector'; import FakerMappingSelector from './faker-mapping-selector'; -import type { FakerSchemaMapping, MockDataGeneratorState } from './types'; +import type { FakerSchema, MockDataGeneratorState } from './types'; const containerStyles = css({ display: 'flex', @@ -49,55 +49,49 @@ const schemaEditorLoaderStyles = css({ }); const FakerSchemaEditorContent = ({ - fakerSchemaMappings, + fakerSchema, onSchemaConfirmed, }: { - fakerSchemaMappings: FakerSchemaMapping[]; + fakerSchema: FakerSchema; onSchemaConfirmed: (isConfirmed: boolean) => void; }) => { const [fakerSchemaFormValues, setFakerSchemaFormValues] = - React.useState>(fakerSchemaMappings); - const [activeField, setActiveField] = React.useState( - fakerSchemaFormValues[0].fieldPath - ); + React.useState(fakerSchema); + + const fieldPaths = Object.keys(fakerSchemaFormValues); + const [activeField, setActiveField] = React.useState(fieldPaths[0]); - const activeJsonType = fakerSchemaFormValues.find( - (mapping) => mapping.fieldPath === activeField - )?.mongoType; - const activeFakerFunction = fakerSchemaFormValues.find( - (mapping) => mapping.fieldPath === activeField - )?.fakerMethod; + const activeJsonType = fakerSchemaFormValues[activeField]?.mongoType; + const activeFakerFunction = fakerSchemaFormValues[activeField]?.fakerMethod; const resetIsSchemaConfirmed = () => { onSchemaConfirmed(false); }; const onJsonTypeSelect = (newJsonType: string) => { - const updatedFakerFieldMapping = fakerSchemaFormValues.find( - (mapping) => mapping.fieldPath === activeField - ); - if (updatedFakerFieldMapping) { - updatedFakerFieldMapping.mongoType = newJsonType; - setFakerSchemaFormValues( - fakerSchemaFormValues.map((mapping) => - mapping.fieldPath === activeField ? updatedFakerFieldMapping : mapping - ) - ); + const currentMapping = fakerSchemaFormValues[activeField]; + if (currentMapping) { + setFakerSchemaFormValues({ + ...fakerSchemaFormValues, + [activeField]: { + ...currentMapping, + mongoType: newJsonType, + }, + }); resetIsSchemaConfirmed(); } }; const onFakerFunctionSelect = (newFakerFunction: string) => { - const updatedFakerFieldMapping = fakerSchemaFormValues.find( - (mapping) => mapping.fieldPath === activeField - ); - if (updatedFakerFieldMapping) { - updatedFakerFieldMapping.fakerMethod = newFakerFunction; - setFakerSchemaFormValues( - fakerSchemaFormValues.map((mapping) => - mapping.fieldPath === activeField ? updatedFakerFieldMapping : mapping - ) - ); + const currentMapping = fakerSchemaFormValues[activeField]; + if (currentMapping) { + setFakerSchemaFormValues({ + ...fakerSchemaFormValues, + [activeField]: { + ...currentMapping, + fakerMethod: newFakerFunction, + }, + }); resetIsSchemaConfirmed(); } }; @@ -107,7 +101,7 @@ const FakerSchemaEditorContent = ({
mapping.fieldPath)} + fields={fieldPaths} onFieldSelect={setActiveField} /> {activeJsonType && activeFakerFunction && ( @@ -163,7 +157,7 @@ const FakerSchemaEditorScreen = ({ )} {fakerSchemaGenerationState.status === 'completed' && ( )} From df1e8f80fb4287e196f71cec531e9ee9fba2d9d6 Mon Sep 17 00:00:00 2001 From: Nataly Carbonell Date: Thu, 18 Sep 2025 14:44:32 -0400 Subject: [PATCH 05/40] add ts error annotation back and more context in docstring --- packages/compass-collection/src/modules/collection-tab.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/compass-collection/src/modules/collection-tab.ts b/packages/compass-collection/src/modules/collection-tab.ts index 615619bc3e4..642b9c92d16 100644 --- a/packages/compass-collection/src/modules/collection-tab.ts +++ b/packages/compass-collection/src/modules/collection-tab.ts @@ -41,6 +41,7 @@ import type { MockDataGeneratorState, } from '../components/mock-data-generator-modal/types'; +// @ts-expect-error TypeScript warns us about importing ESM module from CommonJS module, but we can ignore since this code will be consumed by webpack. import { faker } from '@faker-js/faker/locale/en'; const DEFAULT_SAMPLE_SIZE = 100; @@ -715,6 +716,10 @@ function transformFakerSchemaToObject( /** * Checks if the method exists and is callable on the faker object. + * + * Note: Only supports the format `module.method` (e.g., `internet.email`). + * Nested modules or other formats are not supported. + * @see {@link https://fakerjs.dev/api/} */ function isValidFakerMethod(fakerMethod: string): boolean { const parts = fakerMethod.split('.'); From beecd6bbede0490e00019a4b0e6810e87dfc7b13 Mon Sep 17 00:00:00 2001 From: Nataly Carbonell Date: Thu, 18 Sep 2025 17:23:02 -0400 Subject: [PATCH 06/40] address: update FakerFieldMapping to use MongoDBFieldType and improve type safety --- .../faker-schema-editor-screen.tsx | 3 ++- .../script-generation-utils.spec.ts | 6 ++---- .../script-generation-utils.ts | 8 +------- .../components/mock-data-generator-modal/types.ts | 12 +++++++----- .../compass-collection/src/modules/collection-tab.ts | 6 +++++- 5 files changed, 17 insertions(+), 18 deletions(-) diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/faker-schema-editor-screen.tsx b/packages/compass-collection/src/components/mock-data-generator-modal/faker-schema-editor-screen.tsx index fb6f6ebef50..560dfda3dc4 100644 --- a/packages/compass-collection/src/components/mock-data-generator-modal/faker-schema-editor-screen.tsx +++ b/packages/compass-collection/src/components/mock-data-generator-modal/faker-schema-editor-screen.tsx @@ -13,6 +13,7 @@ import React from 'react'; import FieldSelector from './schema-field-selector'; import FakerMappingSelector from './faker-mapping-selector'; import type { FakerSchema, MockDataGeneratorState } from './types'; +import type { MongoDBFieldType } from '../../schema-analysis-types'; const containerStyles = css({ display: 'flex', @@ -75,7 +76,7 @@ const FakerSchemaEditorContent = ({ ...fakerSchemaFormValues, [activeField]: { ...currentMapping, - mongoType: newJsonType, + mongoType: newJsonType as MongoDBFieldType, }, }); resetIsSchemaConfirmed(); diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.spec.ts b/packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.spec.ts index b7c24288dcf..82dcf6cb1da 100644 --- a/packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.spec.ts +++ b/packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.spec.ts @@ -1,9 +1,7 @@ import { expect } from 'chai'; import { faker } from '@faker-js/faker/locale/en'; -import { - generateScript, - type FakerFieldMapping, -} from './script-generation-utils'; +import { generateScript } from './script-generation-utils'; +import type { FakerFieldMapping } from './types'; /** * Helper function to test that generated document code is executable diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.ts b/packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.ts index 1fcc7b09f8f..45cdb20bc47 100644 --- a/packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.ts +++ b/packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.ts @@ -1,17 +1,11 @@ import type { MongoDBFieldType } from '../../schema-analysis-types'; +import type { FakerFieldMapping } from './types'; export type FakerArg = string | number | boolean | { json: string }; const DEFAULT_ARRAY_LENGTH = 3; const INDENT_SIZE = 2; -export interface FakerFieldMapping { - mongoType: MongoDBFieldType; - fakerMethod: string; - fakerArgs: FakerArg[]; - probability?: number; // 0.0 - 1.0 frequency of field (defaults to 1.0) -} - // Array length configuration for different array types export type ArrayLengthMap = { [fieldName: string]: diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/types.ts b/packages/compass-collection/src/components/mock-data-generator-modal/types.ts index 1d09ccd69cb..6696cf3a52f 100644 --- a/packages/compass-collection/src/components/mock-data-generator-modal/types.ts +++ b/packages/compass-collection/src/components/mock-data-generator-modal/types.ts @@ -1,4 +1,6 @@ import type { MockDataSchemaResponse } from '@mongodb-js/compass-generative-ai'; +import type { MongoDBFieldType } from '../../schema-analysis-types'; +import type { FakerArg } from './script-generation-utils'; export enum MockDataGeneratorStep { SCHEMA_CONFIRMATION = 'SCHEMA_CONFIRMATION', @@ -37,11 +39,11 @@ export type MockDataGeneratorState = export type FakerSchemaMapping = MockDataSchemaResponse['fields'][number]; -export type FakerFieldMapping = { - mongoType: string; +export interface FakerFieldMapping { + mongoType: MongoDBFieldType; fakerMethod: string; - fakerArgs: any[]; - probability: number; -}; + fakerArgs: FakerArg[]; + probability?: number; // 0.0 - 1.0 frequency of field (defaults to 1.0) +} export type FakerSchema = Record; diff --git a/packages/compass-collection/src/modules/collection-tab.ts b/packages/compass-collection/src/modules/collection-tab.ts index 642b9c92d16..b5f90fcbfa2 100644 --- a/packages/compass-collection/src/modules/collection-tab.ts +++ b/packages/compass-collection/src/modules/collection-tab.ts @@ -27,6 +27,7 @@ import { type SchemaAnalysisError, type SchemaAnalysisState, type FieldInfo, + MongoDBFieldType, } from '../schema-analysis-types'; import { calculateSchemaDepth } from '../calculate-schema-depth'; import { @@ -708,7 +709,10 @@ function transformFakerSchemaToObject( for (const field of fakerSchema) { const { fieldPath, ...fieldMapping } = field; - result[fieldPath] = fieldMapping; + result[fieldPath] = { + ...fieldMapping, + mongoType: fieldMapping.mongoType as MongoDBFieldType, + }; } return result; From d1989d993f205e690b65131603e5a927506c16cd Mon Sep 17 00:00:00 2001 From: Nataly Carbonell Date: Thu, 18 Sep 2025 17:37:14 -0400 Subject: [PATCH 07/40] remove @ts-expect-error annotation, annotate type import --- packages/compass-collection/src/modules/collection-tab.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/compass-collection/src/modules/collection-tab.ts b/packages/compass-collection/src/modules/collection-tab.ts index b5f90fcbfa2..214a238b0b2 100644 --- a/packages/compass-collection/src/modules/collection-tab.ts +++ b/packages/compass-collection/src/modules/collection-tab.ts @@ -27,7 +27,7 @@ import { type SchemaAnalysisError, type SchemaAnalysisState, type FieldInfo, - MongoDBFieldType, + type MongoDBFieldType, } from '../schema-analysis-types'; import { calculateSchemaDepth } from '../calculate-schema-depth'; import { @@ -42,7 +42,6 @@ import type { MockDataGeneratorState, } from '../components/mock-data-generator-modal/types'; -// @ts-expect-error TypeScript warns us about importing ESM module from CommonJS module, but we can ignore since this code will be consumed by webpack. import { faker } from '@faker-js/faker/locale/en'; const DEFAULT_SAMPLE_SIZE = 100; From 452d8f80c541c17adbf8dd2435b9f38cfd60fd24 Mon Sep 17 00:00:00 2001 From: Nataly Carbonell Date: Fri, 19 Sep 2025 11:36:16 -0400 Subject: [PATCH 08/40] refactor: update MongoDBFieldType import paths and improve type usage across mock data generator components --- .../faker-mapping-selector.tsx | 5 +++-- .../faker-schema-editor-screen.tsx | 6 ++--- .../mock-data-generator-modal.spec.tsx | 22 +++++++++---------- .../script-generation-utils.ts | 2 +- .../mock-data-generator-modal/types.ts | 2 +- .../src/modules/collection-tab.ts | 3 +-- .../src/schema-analysis-types.ts | 7 +----- .../src/atlas-ai-service.ts | 9 ++++++-- packages/compass-generative-ai/src/index.ts | 1 + 9 files changed, 29 insertions(+), 28 deletions(-) diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/faker-mapping-selector.tsx b/packages/compass-collection/src/components/mock-data-generator-modal/faker-mapping-selector.tsx index 9d64a77e369..b6c1aa726b0 100644 --- a/packages/compass-collection/src/components/mock-data-generator-modal/faker-mapping-selector.tsx +++ b/packages/compass-collection/src/components/mock-data-generator-modal/faker-mapping-selector.tsx @@ -10,6 +10,7 @@ import { } from '@mongodb-js/compass-components'; import React from 'react'; import { UNRECOGNIZED_FAKER_METHOD } from '../../modules/collection-tab'; +import type { MongoDBFieldType } from '@mongodb-js/compass-generative-ai'; const fieldMappingSelectorsStyles = css({ width: '50%', @@ -26,7 +27,7 @@ const labelStyles = css({ interface Props { activeJsonType: string; activeFakerFunction: string; - onJsonTypeSelect: (jsonType: string) => void; + onJsonTypeSelect: (jsonType: MongoDBFieldType) => void; onFakerFunctionSelect: (fakerFunction: string) => void; } @@ -43,7 +44,7 @@ const FakerMappingSelector = ({ label="JSON Type" allowDeselect={false} value={activeJsonType} - onChange={onJsonTypeSelect} + onChange={(value) => onJsonTypeSelect(value as MongoDBFieldType)} > {/* TODO(CLOUDP-344400) : Make the select input editable and render other options depending on the JSON type selected */} {[activeJsonType].map((type) => ( diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/faker-schema-editor-screen.tsx b/packages/compass-collection/src/components/mock-data-generator-modal/faker-schema-editor-screen.tsx index 560dfda3dc4..39b6a5d7a27 100644 --- a/packages/compass-collection/src/components/mock-data-generator-modal/faker-schema-editor-screen.tsx +++ b/packages/compass-collection/src/components/mock-data-generator-modal/faker-schema-editor-screen.tsx @@ -13,7 +13,7 @@ import React from 'react'; import FieldSelector from './schema-field-selector'; import FakerMappingSelector from './faker-mapping-selector'; import type { FakerSchema, MockDataGeneratorState } from './types'; -import type { MongoDBFieldType } from '../../schema-analysis-types'; +import type { MongoDBFieldType } from '@mongodb-js/compass-generative-ai'; const containerStyles = css({ display: 'flex', @@ -69,14 +69,14 @@ const FakerSchemaEditorContent = ({ onSchemaConfirmed(false); }; - const onJsonTypeSelect = (newJsonType: string) => { + const onJsonTypeSelect = (newJsonType: MongoDBFieldType) => { const currentMapping = fakerSchemaFormValues[activeField]; if (currentMapping) { setFakerSchemaFormValues({ ...fakerSchemaFormValues, [activeField]: { ...currentMapping, - mongoType: newJsonType as MongoDBFieldType, + mongoType: newJsonType, }, }); resetIsSchemaConfirmed(); diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.spec.tsx b/packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.spec.tsx index 5c82204ade8..038f4f5e93e 100644 --- a/packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.spec.tsx +++ b/packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.spec.tsx @@ -89,7 +89,7 @@ describe('MockDataGeneratorModal', () => { fields: [ { fieldPath: 'name', - mongoType: 'string', + mongoType: 'String', fakerMethod: 'person.firstName', fakerArgs: [], }, @@ -318,25 +318,25 @@ describe('MockDataGeneratorModal', () => { fields: [ { fieldPath: 'name', - mongoType: 'string', + mongoType: 'String', fakerMethod: 'person.firstName', fakerArgs: [], }, { fieldPath: 'age', - mongoType: 'int', + mongoType: 'Int32', fakerMethod: 'number.int', fakerArgs: [], }, { fieldPath: 'email', - mongoType: 'string', + mongoType: 'String', fakerMethod: 'internet', fakerArgs: [], }, { fieldPath: 'username', - mongoType: 'string', + mongoType: 'String', fakerMethod: 'noSuchMethod', fakerArgs: [], }, @@ -390,21 +390,21 @@ describe('MockDataGeneratorModal', () => { }); // the "name" field should be selected by default expect(screen.getByText('name')).to.exist; - expect(screen.getByLabelText('JSON Type')).to.have.value('string'); + expect(screen.getByLabelText('JSON Type')).to.have.value('String'); expect(screen.getByLabelText('Faker Function')).to.have.value( 'person.firstName' ); // select the "age" field userEvent.click(screen.getByText('age')); expect(screen.getByText('age')).to.exist; - expect(screen.getByLabelText('JSON Type')).to.have.value('int'); + expect(screen.getByLabelText('JSON Type')).to.have.value('Int32'); expect(screen.getByLabelText('Faker Function')).to.have.value( 'number.int' ); // select the "email" field userEvent.click(screen.getByText('email')); expect(screen.getByText('email')).to.exist; - expect(screen.getByLabelText('JSON Type')).to.have.value('string'); + expect(screen.getByLabelText('JSON Type')).to.have.value('String'); // the "email" field should have a warning banner since the faker method is invalid expect(screen.getByLabelText('Faker Function')).to.have.value( 'Unrecognized' @@ -418,7 +418,7 @@ describe('MockDataGeneratorModal', () => { // select the "username" field userEvent.click(screen.getByText('username')); expect(screen.getByText('username')).to.exist; - expect(screen.getByLabelText('JSON Type')).to.have.value('string'); + expect(screen.getByLabelText('JSON Type')).to.have.value('String'); expect(screen.getByLabelText('Faker Function')).to.have.value( 'Unrecognized' ); @@ -432,7 +432,7 @@ describe('MockDataGeneratorModal', () => { fields: [ { fieldPath: 'name', - mongoType: 'string', + mongoType: 'String', fakerMethod: 'person.firstName', fakerArgs: [], isArray: false, @@ -440,7 +440,7 @@ describe('MockDataGeneratorModal', () => { }, { fieldPath: 'email', - mongoType: 'string', + mongoType: 'String', fakerMethod: 'internet.email', fakerArgs: [], isArray: false, diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.ts b/packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.ts index 45cdb20bc47..6d465e6628a 100644 --- a/packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.ts +++ b/packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.ts @@ -1,4 +1,4 @@ -import type { MongoDBFieldType } from '../../schema-analysis-types'; +import type { MongoDBFieldType } from '@mongodb-js/compass-generative-ai'; import type { FakerFieldMapping } from './types'; export type FakerArg = string | number | boolean | { json: string }; diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/types.ts b/packages/compass-collection/src/components/mock-data-generator-modal/types.ts index 6696cf3a52f..9e9bfe9dc50 100644 --- a/packages/compass-collection/src/components/mock-data-generator-modal/types.ts +++ b/packages/compass-collection/src/components/mock-data-generator-modal/types.ts @@ -1,5 +1,5 @@ import type { MockDataSchemaResponse } from '@mongodb-js/compass-generative-ai'; -import type { MongoDBFieldType } from '../../schema-analysis-types'; +import type { MongoDBFieldType } from '@mongodb-js/compass-generative-ai'; import type { FakerArg } from './script-generation-utils'; export enum MockDataGeneratorStep { diff --git a/packages/compass-collection/src/modules/collection-tab.ts b/packages/compass-collection/src/modules/collection-tab.ts index 214a238b0b2..0e91cea1e52 100644 --- a/packages/compass-collection/src/modules/collection-tab.ts +++ b/packages/compass-collection/src/modules/collection-tab.ts @@ -27,7 +27,6 @@ import { type SchemaAnalysisError, type SchemaAnalysisState, type FieldInfo, - type MongoDBFieldType, } from '../schema-analysis-types'; import { calculateSchemaDepth } from '../calculate-schema-depth'; import { @@ -710,7 +709,7 @@ function transformFakerSchemaToObject( const { fieldPath, ...fieldMapping } = field; result[fieldPath] = { ...fieldMapping, - mongoType: fieldMapping.mongoType as MongoDBFieldType, + mongoType: fieldMapping.mongoType, }; } diff --git a/packages/compass-collection/src/schema-analysis-types.ts b/packages/compass-collection/src/schema-analysis-types.ts index 286fc166158..954080599af 100644 --- a/packages/compass-collection/src/schema-analysis-types.ts +++ b/packages/compass-collection/src/schema-analysis-types.ts @@ -1,5 +1,5 @@ +import type { MongoDBFieldType } from '@mongodb-js/compass-generative-ai'; import type { Document } from 'mongodb'; -import type { PrimitiveSchemaType } from 'mongodb-schema'; export const SCHEMA_ANALYSIS_STATE_INITIAL = 'initial'; export const SCHEMA_ANALYSIS_STATE_ANALYZING = 'analyzing'; @@ -30,11 +30,6 @@ export type SchemaAnalysisErrorState = { error: SchemaAnalysisError; }; -/** - * MongoDB schema type - */ -export type MongoDBFieldType = PrimitiveSchemaType['name']; - /** * Primitive values that can appear in sample_values after BSON-to-primitive conversion. * These are the JavaScript primitive equivalents of BSON values. diff --git a/packages/compass-generative-ai/src/atlas-ai-service.ts b/packages/compass-generative-ai/src/atlas-ai-service.ts index 480beb00cd5..23c36a19ae6 100644 --- a/packages/compass-generative-ai/src/atlas-ai-service.ts +++ b/packages/compass-generative-ai/src/atlas-ai-service.ts @@ -1,4 +1,4 @@ -import type { SimplifiedSchema } from 'mongodb-schema'; +import type { PrimitiveSchemaType, SimplifiedSchema } from 'mongodb-schema'; import { type PreferencesAccess, isAIFeatureEnabled, @@ -231,11 +231,16 @@ export interface MockDataSchemaRequest { signal: AbortSignal; } +/** + * MongoDB schema type + */ +export type MongoDBFieldType = PrimitiveSchemaType['name']; + export const MockDataSchemaResponseShape = z.object({ fields: z.array( z.object({ fieldPath: z.string(), - mongoType: z.string(), + mongoType: z.string() as z.ZodType, fakerMethod: z.string(), fakerArgs: z.array( z.union([ diff --git a/packages/compass-generative-ai/src/index.ts b/packages/compass-generative-ai/src/index.ts index 525d5b12008..26218310a07 100644 --- a/packages/compass-generative-ai/src/index.ts +++ b/packages/compass-generative-ai/src/index.ts @@ -35,4 +35,5 @@ export type { MockDataSchemaRequest, MockDataSchemaRawField, MockDataSchemaResponse, + MongoDBFieldType, } from './atlas-ai-service'; From 70f02bf01d4d99011e008a05d1b1e552129f16c7 Mon Sep 17 00:00:00 2001 From: Nataly Carbonell Date: Fri, 19 Sep 2025 11:44:19 -0400 Subject: [PATCH 09/40] remove 'content' field from mock data schema response --- .../mock-data-generator-modal.spec.tsx | 76 +++++++++---------- .../src/modules/collection-tab.ts | 2 +- 2 files changed, 37 insertions(+), 41 deletions(-) diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.spec.tsx b/packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.spec.tsx index 038f4f5e93e..e6952625e70 100644 --- a/packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.spec.tsx +++ b/packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.spec.tsx @@ -428,26 +428,24 @@ describe('MockDataGeneratorModal', () => { const mockServices = createMockServices(); mockServices.atlasAiService.getMockDataSchema = () => Promise.resolve({ - content: { - fields: [ - { - fieldPath: 'name', - mongoType: 'String', - fakerMethod: 'person.firstName', - fakerArgs: [], - isArray: false, - probability: 1.0, - }, - { - fieldPath: 'email', - mongoType: 'String', - fakerMethod: 'internet.email', - fakerArgs: [], - isArray: false, - probability: 1.0, - }, - ], - }, + fields: [ + { + fieldPath: 'name', + mongoType: 'String', + fakerMethod: 'person.firstName', + fakerArgs: [], + isArray: false, + probability: 1.0, + }, + { + fieldPath: 'email', + mongoType: 'String', + fakerMethod: 'internet.email', + fakerArgs: [], + isArray: false, + probability: 1.0, + }, + ], }); await renderModal({ mockServices, @@ -468,26 +466,24 @@ describe('MockDataGeneratorModal', () => { const mockServices = createMockServices(); mockServices.atlasAiService.getMockDataSchema = () => Promise.resolve({ - content: { - fields: [ - { - fieldPath: 'name', - mongoType: 'String', - fakerMethod: 'person.firstName', - fakerArgs: [], - isArray: false, - probability: 1.0, - }, - { - fieldPath: 'age', - mongoType: 'Int32', - fakerMethod: 'number.int', - fakerArgs: [], - isArray: false, - probability: 1.0, - }, - ], - }, + fields: [ + { + fieldPath: 'name', + mongoType: 'String', + fakerMethod: 'person.firstName', + fakerArgs: [], + isArray: false, + probability: 1.0, + }, + { + fieldPath: 'age', + mongoType: 'Int32', + fakerMethod: 'number.int', + fakerArgs: [], + isArray: false, + probability: 1.0, + }, + ], }); await renderModal({ diff --git a/packages/compass-collection/src/modules/collection-tab.ts b/packages/compass-collection/src/modules/collection-tab.ts index 0e91cea1e52..69928d0a8a5 100644 --- a/packages/compass-collection/src/modules/collection-tab.ts +++ b/packages/compass-collection/src/modules/collection-tab.ts @@ -874,7 +874,7 @@ export const generateFakerMappings = (): CollectionThunkAction< const validatedFakerSchema = validateFakerSchema( schemaAnalysis.processedSchema, - response.content.fields, + response.fields, logger ); From a1bdd4db00919c9761b552b5edb1d0754cfb1dc5 Mon Sep 17 00:00:00 2001 From: Nataly Carbonell Date: Fri, 19 Sep 2025 13:32:28 -0400 Subject: [PATCH 10/40] fix: update fakerArgs and probability handling in validateFakerSchema function --- packages/compass-collection/src/modules/collection-tab.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/compass-collection/src/modules/collection-tab.ts b/packages/compass-collection/src/modules/collection-tab.ts index 69928d0a8a5..95c5d35dc5d 100644 --- a/packages/compass-collection/src/modules/collection-tab.ts +++ b/packages/compass-collection/src/modules/collection-tab.ts @@ -788,8 +788,8 @@ const validateFakerSchema = ( result[fieldPath] = { mongoType: fakerMapping.mongoType, fakerMethod: UNRECOGNIZED_FAKER_METHOD, - fakerArgs: fakerMapping.fakerArgs, - probability: fakerMapping.probability, + fakerArgs: [], + probability: inputSchema[fieldPath].probability, }; } } else { @@ -798,7 +798,7 @@ const validateFakerSchema = ( mongoType: inputSchema[fieldPath].type, fakerMethod: UNRECOGNIZED_FAKER_METHOD, fakerArgs: [], - probability: 1, + probability: inputSchema[fieldPath].probability, }; } } From dd22acf1d0bee09494b9bca5ac0ff48bfda80e68 Mon Sep 17 00:00:00 2001 From: Nataly Carbonell Date: Fri, 19 Sep 2025 13:57:16 -0400 Subject: [PATCH 11/40] clean up probability handling in AtlasAiService and in faker schema validation --- .../mock-data-generator-modal.spec.tsx | 8 ++++---- .../compass-collection/src/modules/collection-tab.ts | 11 +++++++---- .../src/atlas-ai-service.spec.ts | 3 --- .../compass-generative-ai/src/atlas-ai-service.ts | 3 +-- 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.spec.tsx b/packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.spec.tsx index e6952625e70..e758e4dbac6 100644 --- a/packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.spec.tsx +++ b/packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.spec.tsx @@ -5,6 +5,7 @@ import { renderWithActiveConnection, waitFor, userEvent, + waitForElementToBeRemoved, } from '@mongodb-js/testing-library-compass'; import { Provider } from 'react-redux'; import { createStore, applyMiddleware } from 'redux'; @@ -511,10 +512,9 @@ describe('MockDataGeneratorModal', () => { // advance to the schema editor step userEvent.click(screen.getByText('Confirm')); - - await waitFor(() => { - expect(screen.getByTestId('faker-schema-editor')).to.exist; - }); + await waitForElementToBeRemoved(() => + screen.queryByTestId('faker-schema-editor-loader') + ); // select the "name" field userEvent.click(screen.getByText('name')); diff --git a/packages/compass-collection/src/modules/collection-tab.ts b/packages/compass-collection/src/modules/collection-tab.ts index 95c5d35dc5d..11cea5110cd 100644 --- a/packages/compass-collection/src/modules/collection-tab.ts +++ b/packages/compass-collection/src/modules/collection-tab.ts @@ -772,9 +772,12 @@ const validateFakerSchema = ( // Process all input schema fields in a single O(n) pass for (const fieldPath of Object.keys(inputSchema)) { - const fakerMapping = fakerSchemaRaw[fieldPath]; - - if (fakerMapping) { + if (fakerSchemaRaw[fieldPath]) { + // input schema field exists in faker schema + const fakerMapping = { + ...fakerSchemaRaw[fieldPath], + probability: inputSchema[fieldPath].probability, + }; // Validate the faker method if (isValidFakerMethod(fakerMapping.fakerMethod)) { result[fieldPath] = fakerMapping; @@ -789,7 +792,7 @@ const validateFakerSchema = ( mongoType: fakerMapping.mongoType, fakerMethod: UNRECOGNIZED_FAKER_METHOD, fakerArgs: [], - probability: inputSchema[fieldPath].probability, + probability: fakerMapping.probability, }; } } else { diff --git a/packages/compass-generative-ai/src/atlas-ai-service.spec.ts b/packages/compass-generative-ai/src/atlas-ai-service.spec.ts index a093b730e47..92ecfe9f3a1 100644 --- a/packages/compass-generative-ai/src/atlas-ai-service.spec.ts +++ b/packages/compass-generative-ai/src/atlas-ai-service.spec.ts @@ -393,12 +393,10 @@ describe('AtlasAiService', function () { name: { type: 'string', sampleValues: ['John', 'Jane', 'Bob'], - probability: 0.9, }, age: { type: 'number', sampleValues: [25, 30, 35], - probability: 0.8, }, }, includeSampleValues: false, @@ -536,7 +534,6 @@ describe('AtlasAiService', function () { ); expect(requestBody.schema.age).to.not.have.property('sampleValues'); expect(requestBody.schema.name.type).to.equal('string'); - expect(requestBody.schema.age.probability).to.equal(0.8); }); it('makes POST request with correct headers and body structure', async function () { diff --git a/packages/compass-generative-ai/src/atlas-ai-service.ts b/packages/compass-generative-ai/src/atlas-ai-service.ts index 23c36a19ae6..cae657a7a5c 100644 --- a/packages/compass-generative-ai/src/atlas-ai-service.ts +++ b/packages/compass-generative-ai/src/atlas-ai-service.ts @@ -218,7 +218,6 @@ const aiURLConfig = { export interface MockDataSchemaRawField { type: string; sampleValues?: unknown[]; - probability?: number; } export interface MockDataSchemaRequest { @@ -475,7 +474,7 @@ export class AtlasAiService { Omit > = {}; for (const [k, v] of Object.entries(schema)) { - newSchema[k] = { type: v.type, probability: v.probability }; + newSchema[k] = { type: v.type }; } schema = newSchema; } From bd3f61a68888ddffc7ce3169f21ac2625bd33e35 Mon Sep 17 00:00:00 2001 From: Nataly Carbonell Date: Fri, 19 Sep 2025 14:42:25 -0400 Subject: [PATCH 12/40] rename FakerSchemaMapping to LlmFakerMapping --- .../src/components/mock-data-generator-modal/types.ts | 2 +- packages/compass-collection/src/modules/collection-tab.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/types.ts b/packages/compass-collection/src/components/mock-data-generator-modal/types.ts index 9e9bfe9dc50..af5150fbd55 100644 --- a/packages/compass-collection/src/components/mock-data-generator-modal/types.ts +++ b/packages/compass-collection/src/components/mock-data-generator-modal/types.ts @@ -37,7 +37,7 @@ export type MockDataGeneratorState = | MockDataGeneratorCompletedState | MockDataGeneratorErrorState; -export type FakerSchemaMapping = MockDataSchemaResponse['fields'][number]; +export type LlmFakerMapping = MockDataSchemaResponse['fields'][number]; export interface FakerFieldMapping { mongoType: MongoDBFieldType; diff --git a/packages/compass-collection/src/modules/collection-tab.ts b/packages/compass-collection/src/modules/collection-tab.ts index 11cea5110cd..9e5e4faf092 100644 --- a/packages/compass-collection/src/modules/collection-tab.ts +++ b/packages/compass-collection/src/modules/collection-tab.ts @@ -36,7 +36,7 @@ import { import type { Document, MongoError } from 'mongodb'; import { MockDataGeneratorStep } from '../components/mock-data-generator-modal/types'; import type { - FakerSchemaMapping, + LlmFakerMapping, FakerSchema, MockDataGeneratorState, } from '../components/mock-data-generator-modal/types'; @@ -701,7 +701,7 @@ export const cancelSchemaAnalysis = (): CollectionThunkAction => { * Moves fieldPath from object property to object key. */ function transformFakerSchemaToObject( - fakerSchema: FakerSchemaMapping[] + fakerSchema: LlmFakerMapping[] ): FakerSchema { const result: FakerSchema = {}; @@ -762,7 +762,7 @@ function isValidFakerMethod(fakerMethod: string): boolean { */ const validateFakerSchema = ( inputSchema: Record, - fakerSchemaArray: FakerSchemaMapping[], + fakerSchemaArray: LlmFakerMapping[], logger: Logger ): FakerSchema => { // Transform to keyed object structure From 6a6307b6bc9d632ee4acfee65f45d593a15f38ea Mon Sep 17 00:00:00 2001 From: Nataly Carbonell Date: Mon, 22 Sep 2025 11:57:19 -0400 Subject: [PATCH 13/40] validate mongoType with zod and move fakerSchema transformation method outside validation method --- .../src/modules/collection-tab.ts | 13 +++++----- .../src/atlas-ai-service.ts | 24 ++++++++++++++++++- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/packages/compass-collection/src/modules/collection-tab.ts b/packages/compass-collection/src/modules/collection-tab.ts index 9e5e4faf092..77720bd4bf3 100644 --- a/packages/compass-collection/src/modules/collection-tab.ts +++ b/packages/compass-collection/src/modules/collection-tab.ts @@ -751,7 +751,6 @@ function isValidFakerMethod(fakerMethod: string): boolean { /** * Validates a given faker schema against an input schema. * - * - Transforms LLM array format to keyed object structure * - Validates the `fakerMethod` for each field, marking it as unrecognized if invalid * - Adds any unmapped input schema fields to the result with an unrecognized faker method * @@ -762,12 +761,9 @@ function isValidFakerMethod(fakerMethod: string): boolean { */ const validateFakerSchema = ( inputSchema: Record, - fakerSchemaArray: LlmFakerMapping[], + fakerSchemaRaw: FakerSchema, logger: Logger ): FakerSchema => { - // Transform to keyed object structure - const fakerSchemaRaw = transformFakerSchemaToObject(fakerSchemaArray); - const result: FakerSchema = {}; // Process all input schema fields in a single O(n) pass @@ -875,9 +871,14 @@ export const generateFakerMappings = (): CollectionThunkAction< connectionInfoRef.current ); + // Transform to keyed object structure + const transformedFakerSchema = transformFakerSchemaToObject( + response.fields + ); + const validatedFakerSchema = validateFakerSchema( schemaAnalysis.processedSchema, - response.fields, + transformedFakerSchema, logger ); diff --git a/packages/compass-generative-ai/src/atlas-ai-service.ts b/packages/compass-generative-ai/src/atlas-ai-service.ts index cae657a7a5c..503ab85f8e3 100644 --- a/packages/compass-generative-ai/src/atlas-ai-service.ts +++ b/packages/compass-generative-ai/src/atlas-ai-service.ts @@ -235,11 +235,33 @@ export interface MockDataSchemaRequest { */ export type MongoDBFieldType = PrimitiveSchemaType['name']; +// TODO(CLOUDP-346699): Export this from mongodb-schema +enum PrimitiveSchemaTypeValues { + String = 'String', + Number = 'Number', + Boolean = 'Boolean', + Date = 'Date', + Int32 = 'Int32', + Decimal128 = 'Decimal128', + Long = 'Long', + ObjectId = 'ObjectId', + RegExp = 'RegExp', + Symbol = 'Symbol', + MaxKey = 'MaxKey', + MinKey = 'MinKey', + Binary = 'Binary', + Code = 'Code', + Timestamp = 'Timestamp', + DBRef = 'DBRef', +} + export const MockDataSchemaResponseShape = z.object({ fields: z.array( z.object({ fieldPath: z.string(), - mongoType: z.string() as z.ZodType, + mongoType: z.custom((val) => + Object.values(PrimitiveSchemaTypeValues).includes(val) + ), fakerMethod: z.string(), fakerArgs: z.array( z.union([ From 333b4bc4233b8018a56ed8801e413e05a2f6bf7f Mon Sep 17 00:00:00 2001 From: Nataly Carbonell Date: Mon, 22 Sep 2025 12:03:36 -0400 Subject: [PATCH 14/40] rename values --- packages/compass-generative-ai/src/atlas-ai-service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/compass-generative-ai/src/atlas-ai-service.ts b/packages/compass-generative-ai/src/atlas-ai-service.ts index 503ab85f8e3..a490d5119a1 100644 --- a/packages/compass-generative-ai/src/atlas-ai-service.ts +++ b/packages/compass-generative-ai/src/atlas-ai-service.ts @@ -236,7 +236,7 @@ export interface MockDataSchemaRequest { export type MongoDBFieldType = PrimitiveSchemaType['name']; // TODO(CLOUDP-346699): Export this from mongodb-schema -enum PrimitiveSchemaTypeValues { +enum MongoDBFieldTypeValues { String = 'String', Number = 'Number', Boolean = 'Boolean', @@ -260,7 +260,7 @@ export const MockDataSchemaResponseShape = z.object({ z.object({ fieldPath: z.string(), mongoType: z.custom((val) => - Object.values(PrimitiveSchemaTypeValues).includes(val) + Object.values(MongoDBFieldTypeValues).includes(val) ), fakerMethod: z.string(), fakerArgs: z.array( From 6a8bb00313e2f44f20d91d40a2c809b4a4572816 Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Mon, 22 Sep 2025 13:08:01 -0400 Subject: [PATCH 15/40] WIP --- .../script-generation-utils.spec.ts | 40 ++++------ .../script-generation-utils.ts | 60 +++++--------- .../transform-schema-to-field-info.spec.ts | 59 ++++++++++---- .../src/transform-schema-to-field-info.ts | 79 ++++++++++++++++--- 4 files changed, 147 insertions(+), 91 deletions(-) diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.spec.ts b/packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.spec.ts index 82dcf6cb1da..db713342781 100644 --- a/packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.spec.ts +++ b/packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.spec.ts @@ -426,7 +426,7 @@ describe('Script Generation', () => { collectionName: 'posts', documentCount: 1, arrayLengthMap: { - tags: [5], + 'tags[]': 5, }, }); @@ -455,12 +455,8 @@ describe('Script Generation', () => { collectionName: 'groups', documentCount: 1, arrayLengthMap: { - users: { - length: 5, - elements: { - tags: [4], - }, - }, + 'users[]': 5, + 'users[].tags[]': 4, }, }); @@ -495,8 +491,8 @@ describe('Script Generation', () => { collectionName: 'posts', documentCount: 1, arrayLengthMap: { - tags: [0], - categories: [2], + 'tags[]': 0, + 'categories[]': 2, }, }); @@ -530,8 +526,11 @@ describe('Script Generation', () => { collectionName: 'data', documentCount: 1, arrayLengthMap: { - matrix: [2, 5], // 2x5 matrix - cube: [3, 4, 2], // 3x4x2 cube + 'matrix[]': 2, + 'matrix[][]': 5, + 'cube[]': 3, + 'cube[][]': 4, + 'cube[][][]': 2, }, }); @@ -562,19 +561,12 @@ describe('Script Generation', () => { collectionName: 'complex', documentCount: 1, arrayLengthMap: { - users: { - length: 2, - elements: { - tags: [3], - posts: { - length: 4, - elements: { - comments: [5], - }, - }, - }, - }, - matrix: [2, 3], + 'users[]': 2, + 'users[].tags[]': 3, + 'users[].posts[]': 4, + 'users[].posts[].comments[]': 5, + 'matrix[]': 2, + 'matrix[][]': 3, }, }); diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.ts b/packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.ts index 6d465e6628a..029b703c828 100644 --- a/packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.ts +++ b/packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.ts @@ -6,17 +6,7 @@ export type FakerArg = string | number | boolean | { json: string }; const DEFAULT_ARRAY_LENGTH = 3; const INDENT_SIZE = 2; -// Array length configuration for different array types -export type ArrayLengthMap = { - [fieldName: string]: - | number[] // Multi-dimensional: [2, 3, 4] - | ArrayObjectConfig; // Array of objects -}; - -export interface ArrayObjectConfig { - length?: number; // Length of the parent array (optional for nested object containers) - elements: ArrayLengthMap; // Configuration for nested arrays -} +export type ArrayLengthMap = Record; export interface ScriptOptions { documentCount: number; @@ -54,7 +44,7 @@ export function generateScript( const documentCode = renderDocumentCode( structure, INDENT_SIZE * 2, // 4 spaces: 2 for function body + 2 for inside return statement - options.arrayLengthMap + options.arrayLengthMap || {} ); const script = `// Mock Data Generator Script @@ -312,7 +302,8 @@ function insertIntoStructure( function renderDocumentCode( structure: DocumentStructure, indent: number = INDENT_SIZE, - arrayLengthMap: ArrayLengthMap = {} + arrayLengthMap: ArrayLengthMap = {}, + currentPath: string = '' ): string { // For each field in structure: // - If FakerFieldMapping: generate faker call @@ -354,12 +345,15 @@ function renderDocumentCode( } } else if ('type' in value && value.type === 'array') { // It's an array + const fieldPath = currentPath + ? `${currentPath}.${fieldName}[]` + : `${fieldName}[]`; const arrayCode = renderArrayCode( value as ArrayStructure, indent + INDENT_SIZE, fieldName, arrayLengthMap, - 0 // Start at dimension 0 + fieldPath ); documentFields.push( `${fieldIndent}${formatFieldName(fieldName)}: ${arrayCode}` @@ -367,18 +361,15 @@ function renderDocumentCode( } else { // It's a nested object: recursive call - // Get nested array length map for this field, - // including type validation and fallback for malformed maps - const arrayInfo = arrayLengthMap[fieldName]; - const nestedArrayLengthMap = - arrayInfo && !Array.isArray(arrayInfo) && 'elements' in arrayInfo - ? arrayInfo.elements - : {}; + const nestedPath = currentPath + ? `${currentPath}.${fieldName}` + : fieldName; const nestedCode = renderDocumentCode( value as DocumentStructure, indent + INDENT_SIZE, - nestedArrayLengthMap + arrayLengthMap, + nestedPath ); documentFields.push( `${fieldIndent}${formatFieldName(fieldName)}: ${nestedCode}` @@ -418,20 +409,14 @@ function renderArrayCode( indent: number = INDENT_SIZE, fieldName: string = '', arrayLengthMap: ArrayLengthMap = {}, - dimensionIndex: number = 0 + currentFieldPath: string = '' ): string { const elementType = arrayStructure.elementType; // Get array length for this dimension - const arrayInfo = arrayLengthMap[fieldName]; let arrayLength = DEFAULT_ARRAY_LENGTH; - - if (Array.isArray(arrayInfo)) { - // single or multi-dimensional array: eg. [2, 3, 4] or [6] - arrayLength = arrayInfo[dimensionIndex] ?? DEFAULT_ARRAY_LENGTH; // Fallback for malformed array map - } else if (arrayInfo && 'length' in arrayInfo) { - // Array of objects/documents - arrayLength = arrayInfo.length ?? DEFAULT_ARRAY_LENGTH; + if (currentFieldPath && arrayLengthMap[currentFieldPath] !== undefined) { + arrayLength = arrayLengthMap[currentFieldPath]; } if ('mongoType' in elementType) { @@ -439,25 +424,22 @@ function renderArrayCode( const fakerCall = generateFakerCall(elementType as FakerFieldMapping); return `Array.from({length: ${arrayLength}}, () => ${fakerCall})`; } else if ('type' in elementType && elementType.type === 'array') { - // Nested array (e.g., matrix[][]) - keep same fieldName, increment dimension + // Nested array (e.g., matrix[][]) - append another [] to the path + const fieldPath = currentFieldPath + '[]'; const nestedArrayCode = renderArrayCode( elementType as ArrayStructure, indent, fieldName, arrayLengthMap, - dimensionIndex + 1 // Next dimension + fieldPath ); return `Array.from({length: ${arrayLength}}, () => ${nestedArrayCode})`; } else { - // Array of objects - const nestedArrayLengthMap = - arrayInfo && !Array.isArray(arrayInfo) && 'elements' in arrayInfo - ? arrayInfo.elements - : {}; // Fallback to empty map for malformed array map const objectCode = renderDocumentCode( elementType as DocumentStructure, indent, - nestedArrayLengthMap + arrayLengthMap, + currentFieldPath ); return `Array.from({length: ${arrayLength}}, () => (${objectCode}))`; } diff --git a/packages/compass-collection/src/transform-schema-to-field-info.spec.ts b/packages/compass-collection/src/transform-schema-to-field-info.spec.ts index 06bd64de345..47299390aba 100644 --- a/packages/compass-collection/src/transform-schema-to-field-info.spec.ts +++ b/packages/compass-collection/src/transform-schema-to-field-info.spec.ts @@ -53,13 +53,14 @@ describe('processSchema', function () { const result = processSchema(schema); - expect(result).to.deep.equal({ + expect(result.fieldInfo).to.deep.equal({ mixed: { type: 'String', // Should pick the most probable type sample_values: ['text'], probability: 1.0, }, }); + expect(result.arrayLengthMap).to.deep.equal({}); }); it('filters out undefined and null types', function () { @@ -103,13 +104,14 @@ describe('processSchema', function () { const result = processSchema(schema); - expect(result).to.deep.equal({ + expect(result.fieldInfo).to.deep.equal({ optional: { type: 'String', sample_values: ['value'], probability: 0.67, }, }); + expect(result.arrayLengthMap).to.deep.equal({}); }); it('handles fields with no types', function () { @@ -130,7 +132,8 @@ describe('processSchema', function () { const result = processSchema(schema); - expect(result).to.deep.equal({}); + expect(result.fieldInfo).to.deep.equal({}); + expect(result.arrayLengthMap).to.deep.equal({}); }); it('handles empty schema', function () { @@ -141,7 +144,8 @@ describe('processSchema', function () { const result = processSchema(schema); - expect(result).to.deep.equal({}); + expect(result.fieldInfo).to.deep.equal({}); + expect(result.arrayLengthMap).to.deep.equal({}); }); it('limits sample values to 10', function () { @@ -173,8 +177,10 @@ describe('processSchema', function () { const result = processSchema(schema); - expect(result.field.sample_values).to.have.length(10); - expect(result.field.sample_values).to.deep.equal(manyValues.slice(0, 10)); + expect(result.fieldInfo.field.sample_values).to.have.length(10); + expect(result.fieldInfo.field.sample_values).to.deep.equal( + manyValues.slice(0, 10) + ); }); it('transforms simple primitive fields', function () { @@ -258,7 +264,7 @@ describe('processSchema', function () { const result = processSchema(schema); - expect(result).to.deep.equal({ + expect(result.fieldInfo).to.deep.equal({ name: { type: 'String', sample_values: ['John', 'Jane', 'Bob'], @@ -280,6 +286,7 @@ describe('processSchema', function () { probability: 0.7, }, }); + expect(result.arrayLengthMap).to.deep.equal({}); }); it('handles various BSON types', function () { @@ -471,7 +478,7 @@ describe('processSchema', function () { const result = processSchema(schema); - expect(result).to.deep.equal({ + expect(result.fieldInfo).to.deep.equal({ objectId: { type: 'ObjectId', sample_values: ['642d766b7300158b1f22e972'], @@ -523,6 +530,7 @@ describe('processSchema', function () { probability: 1.0, }, }); + expect(result.arrayLengthMap).to.deep.equal({}); }); it('transforms nested document field', function () { @@ -589,7 +597,7 @@ describe('processSchema', function () { const result = processSchema(schema); - expect(result).to.deep.equal({ + expect(result.fieldInfo).to.deep.equal({ 'user.name': { type: 'String', sample_values: ['John'], @@ -601,6 +609,7 @@ describe('processSchema', function () { probability: 0.8, }, }); + expect(result.arrayLengthMap).to.deep.equal({}); }); it('transforms array field', function () { @@ -643,13 +652,16 @@ describe('processSchema', function () { const result = processSchema(schema); - expect(result).to.deep.equal({ + expect(result.fieldInfo).to.deep.equal({ 'tags[]': { type: 'String', sample_values: ['red', 'blue', 'green'], probability: 1.0, }, }); + expect(result.arrayLengthMap).to.deep.equal({ + 'tags[]': 2, // Math.round(1.5) = 2 + }); }); it('handles deeply nested objects (documents)', function () { @@ -717,13 +729,14 @@ describe('processSchema', function () { const result = processSchema(schema); - expect(result).to.deep.equal({ + expect(result.fieldInfo).to.deep.equal({ 'level1.level2.value': { type: 'String', sample_values: ['deep'], probability: 1.0, }, }); + expect(result.arrayLengthMap).to.deep.equal({}); }); it('handles arrays of documents', function () { @@ -802,7 +815,7 @@ describe('processSchema', function () { const result = processSchema(schema); - expect(result).to.deep.equal({ + expect(result.fieldInfo).to.deep.equal({ 'items[].id': { type: 'Number', sample_values: [1, 2], @@ -814,6 +827,9 @@ describe('processSchema', function () { probability: 1.0, }, }); + expect(result.arrayLengthMap).to.deep.equal({ + 'items[]': 2, // averageLength: 2 + }); }); it('handles triple nested arrays (3D matrix)', function () { @@ -889,13 +905,18 @@ describe('processSchema', function () { const result = processSchema(schema); - expect(result).to.deep.equal({ + expect(result.fieldInfo).to.deep.equal({ 'cube[][][]': { type: 'Number', sample_values: [1, 2, 3, 4, 5, 6, 7, 8], probability: 1.0, }, }); + expect(result.arrayLengthMap).to.deep.equal({ + 'cube[]': 2, + 'cube[][]': 2, + 'cube[][][]': 2, + }); }); it('handles arrays of arrays of documents', function () { @@ -988,7 +1009,7 @@ describe('processSchema', function () { const result = processSchema(schema); - expect(result).to.deep.equal({ + expect(result.fieldInfo).to.deep.equal({ 'matrix[][].x': { type: 'Number', sample_values: [1, 3], @@ -1000,6 +1021,10 @@ describe('processSchema', function () { probability: 1.0, }, }); + expect(result.arrayLengthMap).to.deep.equal({ + 'matrix[]': 2, + 'matrix[][]': 1, + }); }); it('handles array of documents with nested arrays', function () { @@ -1092,7 +1117,7 @@ describe('processSchema', function () { const result = processSchema(schema); - expect(result).to.deep.equal({ + expect(result.fieldInfo).to.deep.equal({ 'teams[].name': { type: 'String', sample_values: ['Team A', 'Team B'], @@ -1104,6 +1129,10 @@ describe('processSchema', function () { probability: 1.0, }, }); + expect(result.arrayLengthMap).to.deep.equal({ + 'teams[]': 2, + 'teams[].members[]': 2, // Math.round(1.5) = 2 + }); }); /** diff --git a/packages/compass-collection/src/transform-schema-to-field-info.ts b/packages/compass-collection/src/transform-schema-to-field-info.ts index f88dfb1e6a1..18200f87c47 100644 --- a/packages/compass-collection/src/transform-schema-to-field-info.ts +++ b/packages/compass-collection/src/transform-schema-to-field-info.ts @@ -8,6 +8,7 @@ import type { ConstantSchemaType, } from 'mongodb-schema'; import type { FieldInfo, SampleValue } from './schema-analysis-types'; +import type { ArrayLengthMap } from './components/mock-data-generator-modal/script-generation-utils'; import { ObjectId, Binary, @@ -45,6 +46,27 @@ import { const MAX_SAMPLE_VALUES = 10; export const FIELD_NAME_SEPARATOR = '.'; +/** + * Default array length to use when no specific length information is available + */ +const DEFAULT_ARRAY_LENGTH = 3; + +/** + * Calculate array length from ArraySchemaType, using averageLength with bounds + */ +function calculateArrayLength(arrayType: ArraySchemaType): number { + const avgLength = arrayType.averageLength || DEFAULT_ARRAY_LENGTH; + return Math.max(1, Math.min(50, Math.round(avgLength))); +} + +/** + * Result of processing a schema, including both field information and array length configuration + */ +export interface ProcessSchemaResult { + fieldInfo: Record; + arrayLengthMap: ArrayLengthMap; +} + export class ProcessSchemaUnsupportedStateError extends Error { constructor(message: string) { super(message); @@ -137,27 +159,38 @@ function isPrimitiveSchemaType(type: SchemaType): type is PrimitiveSchemaType { /** * Transforms a raw mongodb-schema Schema into a flat Record * using dot notation for nested fields and bracket notation for arrays. + * Also extracts array length information for script generation. * - * The result is used for the Mock Data Generator LLM call. + * The result is used for the Mock Data Generator LLM call and script generation. */ -export function processSchema(schema: Schema): Record { - const result: Record = {}; +export function processSchema(schema: Schema): ProcessSchemaResult { + const fieldInfo: Record = {}; + const arrayLengthMap: ArrayLengthMap = {}; if (!schema.fields) { - return result; + return { fieldInfo, arrayLengthMap }; } // Process each top-level field for (const field of schema.fields) { - processNamedField(field, '', result); + processNamedField(field, '', fieldInfo, arrayLengthMap); } // post-processing validation - for (const fieldPath of Object.keys(result)) { + for (const fieldPath of Object.keys(fieldInfo)) { validateFieldPath(fieldPath); } - return result; + return { fieldInfo, arrayLengthMap }; +} + +/** + * Legacy function for backward compatibility - returns only the field info + * @deprecated Use processSchema() instead which returns both fieldInfo and arrayLengthMap + */ +export function processSchemaLegacy(schema: Schema): Record { + const result = processSchema(schema); + return result.fieldInfo; } /** @@ -166,7 +199,8 @@ export function processSchema(schema: Schema): Record { function processNamedField( field: SchemaField, pathPrefix: string, - result: Record + result: Record, + arrayLengthMap: ArrayLengthMap ): void { if (!field.types || field.types.length === 0) { return; @@ -187,7 +221,13 @@ function processNamedField( const currentPath = pathPrefix ? `${pathPrefix}.${field.name}` : field.name; // Process based on the type - processType(primaryType, currentPath, result, field.probability); + processType( + primaryType, + currentPath, + result, + field.probability, + arrayLengthMap + ); } /** @@ -197,14 +237,15 @@ function processType( type: SchemaType, currentPath: string, result: Record, - fieldProbability: number + fieldProbability: number, + arrayLengthMap: ArrayLengthMap ): void { if (isConstantSchemaType(type)) { return; } if (isArraySchemaType(type)) { - // Array: add [] to path and recurse into element type + // Array: add [] to path and collect array length information const elementType = getMostFrequentType(type.types || []); if (!elementType) { @@ -212,12 +253,24 @@ function processType( } const arrayPath = `${currentPath}[]`; - processType(elementType, arrayPath, result, fieldProbability); + + // Collect array length information + const arrayLength = calculateArrayLength(type); + arrayLengthMap[arrayPath] = arrayLength; + + // Recurse into element type + processType( + elementType, + arrayPath, + result, + fieldProbability, + arrayLengthMap + ); } else if (isDocumentSchemaType(type)) { // Document: Process nested document fields if (type.fields) { for (const nestedField of type.fields) { - processNamedField(nestedField, currentPath, result); + processNamedField(nestedField, currentPath, result, arrayLengthMap); } } } else if (isPrimitiveSchemaType(type)) { From 3ddc648c8f0a822a9253116e54bf0fd3b297970e Mon Sep 17 00:00:00 2001 From: Nataly Carbonell Date: Mon, 22 Sep 2025 13:36:04 -0400 Subject: [PATCH 16/40] fix generative-ai tests --- .../src/atlas-ai-service.spec.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/compass-generative-ai/src/atlas-ai-service.spec.ts b/packages/compass-generative-ai/src/atlas-ai-service.spec.ts index 92ecfe9f3a1..008a6ad772a 100644 --- a/packages/compass-generative-ai/src/atlas-ai-service.spec.ts +++ b/packages/compass-generative-ai/src/atlas-ai-service.spec.ts @@ -429,13 +429,13 @@ describe('AtlasAiService', function () { fields: [ { fieldPath: 'name', - mongoType: 'string', + mongoType: 'String', fakerMethod: 'person.fullName', fakerArgs: [], }, { fieldPath: 'age', - mongoType: 'int', + mongoType: 'Int32', fakerMethod: 'number.int', fakerArgs: [{ json: '{"min": 18, "max": 65}' }], }, @@ -464,13 +464,13 @@ describe('AtlasAiService', function () { fields: [ { fieldPath: 'name', - mongoType: 'string', + mongoType: 'String', fakerMethod: 'person.fullName', fakerArgs: [], }, { fieldPath: 'age', - mongoType: 'int', + mongoType: 'Int32', fakerMethod: 'number.int', fakerArgs: [{ json: '{"min": 18, "max": 122}' }], }, @@ -504,13 +504,13 @@ describe('AtlasAiService', function () { fields: [ { fieldPath: 'name', - mongoType: 'string', + mongoType: 'String', fakerMethod: 'person.fullName', fakerArgs: [], }, { fieldPath: 'age', - mongoType: 'int', + mongoType: 'Int32', fakerMethod: 'number.int', fakerArgs: [{ json: '{"min": 18, "max": 65}' }], }, @@ -541,13 +541,13 @@ describe('AtlasAiService', function () { fields: [ { fieldPath: 'name', - mongoType: 'string', + mongoType: 'String', fakerMethod: 'person.fullName', fakerArgs: [], }, { fieldPath: 'age', - mongoType: 'int', + mongoType: 'Int32', fakerMethod: 'number.int', fakerArgs: [{ json: '{"min": 18, "max": 65}' }], }, From b93c1bca0d540a77efc154e390c3d806c1ec836c Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Mon, 22 Sep 2025 13:37:11 -0400 Subject: [PATCH 17/40] More tests --- .../script-generation-utils.spec.ts | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.spec.ts b/packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.spec.ts index db713342781..07f013e9fab 100644 --- a/packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.spec.ts +++ b/packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.spec.ts @@ -590,6 +590,69 @@ describe('Script Generation', () => { testDocumentCodeExecution(result.script); } }); + + it('should handle field names with [] in middle (not array notation)', () => { + const schema = { + 'brackets[]InMiddle': createFieldMapping('lorem.word'), + 'items[].nested[]ArrayFieldWithBrackets[]': + createFieldMapping('lorem.sentence'), + 'matrix[]WithBrackets[][]': createFieldMapping('number.int'), + }; + + const result = generateScript(schema, { + databaseName: 'testdb', + collectionName: 'edgecases', + documentCount: 1, + arrayLengthMap: { + 'items[]': 2, + 'items[].nested[]ArrayFieldWithBrackets[]': 3, + 'matrix[]WithBrackets[]': 2, + 'matrix[]WithBrackets[][]': 4, + }, + }); + + expect(result.success).to.equal(true); + if (result.success) { + // Verify field names with [] in middle are treated as regular field names + expect(result.script).to.contain( + '"brackets[]InMiddle": faker.lorem.word()' + ); + + // Verify array of objects with bracket field names containing arrays + expect(result.script).to.contain( + 'items: Array.from({length: 2}, () => ({\n "nested[]ArrayFieldWithBrackets": Array.from({length: 3}, () => faker.lorem.sentence())' + ); + + // Verify multi-dimensional arrays with bracket field names + expect(result.script).to.contain( + '"matrix[]WithBrackets": Array.from({length: 2}, () => Array.from({length: 4}, () => faker.number.int()))' + ); + + // Test that the generated document code is executable + const document = testDocumentCodeExecution(result.script); + expect(document).to.be.an('object'); + + // Verify the three specific edge cases + expect(document).to.have.property('brackets[]InMiddle'); + + expect(document).to.have.property('items'); + expect(document.items).to.be.an('array').with.length(2); + expect(document.items[0]).to.have.property( + 'nested[]ArrayFieldWithBrackets' + ); + expect(document.items[0]['nested[]ArrayFieldWithBrackets']) + .to.be.an('array') + .with.length(3); + + expect(document).to.have.property('matrix[]WithBrackets'); + expect(document['matrix[]WithBrackets']) + .to.be.an('array') + .with.length(2); + expect(document['matrix[]WithBrackets'][0]) + .to.be.an('array') + .with.length(4); + } + }); }); describe('Unrecognized Field Defaults', () => { From 35e172412eaa662a5a6699d25767f33a6a103c41 Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Mon, 22 Sep 2025 14:01:30 -0400 Subject: [PATCH 18/40] WIP --- .../transform-schema-to-field-info.spec.ts | 121 ++++++++++++++++++ .../src/transform-schema-to-field-info.ts | 26 ++-- 2 files changed, 136 insertions(+), 11 deletions(-) diff --git a/packages/compass-collection/src/transform-schema-to-field-info.spec.ts b/packages/compass-collection/src/transform-schema-to-field-info.spec.ts index 47299390aba..a38e4fe32de 100644 --- a/packages/compass-collection/src/transform-schema-to-field-info.spec.ts +++ b/packages/compass-collection/src/transform-schema-to-field-info.spec.ts @@ -1219,4 +1219,125 @@ describe('processSchema', function () { ); }); }); + + describe('Array Length Map', function () { + it('should handle array length bounds (min 1, max 50)', function () { + const schema: Schema = { + fields: [ + { + name: 'smallArray', + path: ['smallArray'], + count: 1, + type: ['Array'], + probability: 1.0, + hasDuplicates: false, + types: [ + { + name: 'Array', + bsonType: 'Array', + path: ['smallArray'], + count: 1, + probability: 1.0, + lengths: [0.3], // Very small average + averageLength: 0.3, + totalCount: 1, + types: [ + { + name: 'String', + bsonType: 'String', + path: ['smallArray'], + count: 1, + probability: 1.0, + values: ['test'], + }, + ], + }, + ], + }, + { + name: 'largeArray', + path: ['largeArray'], + count: 1, + type: ['Array'], + probability: 1.0, + hasDuplicates: false, + types: [ + { + name: 'Array', + bsonType: 'Array', + path: ['largeArray'], + count: 1, + probability: 1.0, + lengths: [100], // Very large average + averageLength: 100, + totalCount: 100, + types: [ + { + name: 'Number', + bsonType: 'Number', + path: ['largeArray'], + count: 100, + probability: 1.0, + values: [new Int32(1)], + }, + ], + }, + ], + }, + ], + count: 1, + }; + + const result = processSchema(schema); + + expect(result.arrayLengthMap).to.deep.equal({ + 'smallArray[]': 1, // Min 1 + 'largeArray[]': 50, // Max 50 + }); + }); + + it('should handle missing averageLength with default', function () { + const schema: Schema = { + fields: [ + { + name: 'defaultArray', + path: ['defaultArray'], + count: 1, + type: ['Array'], + probability: 1.0, + hasDuplicates: false, + types: [ + { + name: 'Array', + bsonType: 'Array', + path: ['defaultArray'], + count: 1, + probability: 1.0, + lengths: [2], + // averageLength is undefined + totalCount: 2, + types: [ + { + name: 'String', + bsonType: 'String', + path: ['defaultArray'], + count: 2, + probability: 1.0, + values: ['a', 'b'], + }, + ], + }, + ], + }, + ], + count: 1, + }; + + const result = processSchema(schema); + + expect(result.arrayLengthMap).to.deep.equal({ + 'defaultArray[]': 3, // DEFAULT_ARRAY_LENGTH = 3 + }); + }); + }); }); diff --git a/packages/compass-collection/src/transform-schema-to-field-info.ts b/packages/compass-collection/src/transform-schema-to-field-info.ts index 18200f87c47..760f8c4a7e1 100644 --- a/packages/compass-collection/src/transform-schema-to-field-info.ts +++ b/packages/compass-collection/src/transform-schema-to-field-info.ts @@ -51,12 +51,25 @@ export const FIELD_NAME_SEPARATOR = '.'; */ const DEFAULT_ARRAY_LENGTH = 3; +/** + * Minimum allowed array length + */ +const MIN_ARRAY_LENGTH = 1; + +/** + * Maximum allowed array length + */ +const MAX_ARRAY_LENGTH = 50; + /** * Calculate array length from ArraySchemaType, using averageLength with bounds */ function calculateArrayLength(arrayType: ArraySchemaType): number { - const avgLength = arrayType.averageLength || DEFAULT_ARRAY_LENGTH; - return Math.max(1, Math.min(50, Math.round(avgLength))); + const avgLength = arrayType.averageLength ?? DEFAULT_ARRAY_LENGTH; + return Math.max( + MIN_ARRAY_LENGTH, + Math.min(MAX_ARRAY_LENGTH, Math.round(avgLength)) + ); } /** @@ -184,15 +197,6 @@ export function processSchema(schema: Schema): ProcessSchemaResult { return { fieldInfo, arrayLengthMap }; } -/** - * Legacy function for backward compatibility - returns only the field info - * @deprecated Use processSchema() instead which returns both fieldInfo and arrayLengthMap - */ -export function processSchemaLegacy(schema: Schema): Record { - const result = processSchema(schema); - return result.fieldInfo; -} - /** * Processes a schema field and its nested types */ From 6324e57cea4fca7dbfadc33c2a2c49af418cb549 Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Mon, 22 Sep 2025 14:15:08 -0400 Subject: [PATCH 19/40] Types --- .../mock-data-generator-modal/to-simplified-field-info.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/to-simplified-field-info.ts b/packages/compass-collection/src/components/mock-data-generator-modal/to-simplified-field-info.ts index 056dad9d670..e15e79d84e7 100644 --- a/packages/compass-collection/src/components/mock-data-generator-modal/to-simplified-field-info.ts +++ b/packages/compass-collection/src/components/mock-data-generator-modal/to-simplified-field-info.ts @@ -1,10 +1,10 @@ import { FIELD_NAME_SEPARATOR } from '../../transform-schema-to-field-info'; import type { processSchema } from '../../transform-schema-to-field-info'; -import type { FieldInfo } from '../../schema-analysis-types'; +import type { PrimitiveSchemaType } from 'mongodb-schema'; type UserFriendlyFieldInfoNode = | { [field: string]: UserFriendlyFieldInfoNode } - | FieldInfo['type']; + | PrimitiveSchemaType['name']; export type SimplifiedFieldInfoTree = { [field: string]: UserFriendlyFieldInfoNode; }; @@ -14,7 +14,7 @@ export type SimplifiedFieldInfoTree = { * ensuring that the user sees a simplification of what the LLM processes. */ export default function toSimplifiedFieldInfo( - input: ReturnType + input: ReturnType['fieldInfo'] ): SimplifiedFieldInfoTree { // ensure parent nodes are created before their children const sortedFieldPaths = Object.keys(input).sort( From 1f6131bdc157acc9cd06ebac39324eb7f985580d Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Mon, 22 Sep 2025 14:50:05 -0400 Subject: [PATCH 20/40] WIP --- .../mock-data-generator-modal.spec.tsx | 67 +++++++++++++++++-- .../mock-data-generator-modal.tsx | 36 +++++++++- .../script-screen.tsx | 44 ++++++++++-- .../src/modules/collection-tab.ts | 7 +- .../src/schema-analysis-types.ts | 1 + 5 files changed, 141 insertions(+), 14 deletions(-) diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.spec.tsx b/packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.spec.tsx index e758e4dbac6..21ef9bdb9bf 100644 --- a/packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.spec.tsx +++ b/packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.spec.tsx @@ -28,6 +28,7 @@ const defaultSchemaAnalysisState: SchemaAnalysisState = { sample_values: ['John', 'Jane'], }, }, + arrayLengthMap: {}, sampleDocument: { name: 'John' }, schemaMetadata: { maxNestingDepth: 1, validationRules: null }, }; @@ -39,6 +40,7 @@ describe('MockDataGeneratorModal', () => { enableGenAISampleDocumentPassing = false, mockServices = createMockServices(), schemaAnalysis = defaultSchemaAnalysisState, + fakerSchemaGeneration = { status: 'idle' }, connectionInfo, }: { isOpen?: boolean; @@ -47,15 +49,14 @@ describe('MockDataGeneratorModal', () => { mockServices?: any; connectionInfo?: ConnectionInfo; schemaAnalysis?: SchemaAnalysisState; + fakerSchemaGeneration?: CollectionState['fakerSchemaGeneration']; } = {}) { const initialState: CollectionState = { workspaceTabId: 'test-workspace-tab-id', namespace: 'test.collection', metadata: null, schemaAnalysis, - fakerSchemaGeneration: { - status: 'idle', - }, + fakerSchemaGeneration, mockDataGenerator: { isModalOpen: isOpen, currentStep: currentStep, @@ -629,7 +630,21 @@ describe('MockDataGeneratorModal', () => { describe('on the generate data step', () => { it('enables the Back button', async () => { - await renderModal({ currentStep: MockDataGeneratorStep.GENERATE_DATA }); + await renderModal({ + currentStep: MockDataGeneratorStep.GENERATE_DATA, + fakerSchemaGeneration: { + status: 'completed', + fakerSchema: { + name: { + fakerMethod: 'person.firstName', + fakerArgs: [], + probability: 1.0, + mongoType: 'String', + }, + }, + requestId: 'test-request-id', + }, + }); expect( screen @@ -639,7 +654,21 @@ describe('MockDataGeneratorModal', () => { }); it('renders the main sections: Prerequisites, steps, and Resources', async () => { - await renderModal({ currentStep: MockDataGeneratorStep.GENERATE_DATA }); + await renderModal({ + currentStep: MockDataGeneratorStep.GENERATE_DATA, + fakerSchemaGeneration: { + status: 'completed', + fakerSchema: { + name: { + fakerMethod: 'person.firstName', + fakerArgs: [], + probability: 1.0, + mongoType: 'String', + }, + }, + requestId: 'test-request-id', + }, + }); expect(screen.getByText('Prerequisites')).to.exist; expect(screen.getByText('1. Create a .js file with the following script')) @@ -649,7 +678,21 @@ describe('MockDataGeneratorModal', () => { }); it('closes the modal when the Done button is clicked', async () => { - await renderModal({ currentStep: MockDataGeneratorStep.GENERATE_DATA }); + await renderModal({ + currentStep: MockDataGeneratorStep.GENERATE_DATA, + fakerSchemaGeneration: { + status: 'completed', + fakerSchema: { + name: { + fakerMethod: 'person.firstName', + fakerArgs: [], + probability: 1.0, + mongoType: 'String', + }, + }, + requestId: 'test-request-id', + }, + }); expect(screen.getByTestId('generate-mock-data-modal')).to.exist; userEvent.click(screen.getByText('Done')); @@ -684,6 +727,18 @@ describe('MockDataGeneratorModal', () => { await renderModal({ currentStep: MockDataGeneratorStep.GENERATE_DATA, connectionInfo: atlasConnectionInfo, + fakerSchemaGeneration: { + status: 'completed', + fakerSchema: { + name: { + fakerMethod: 'person.firstName', + fakerArgs: [], + probability: 1.0, + mongoType: 'String', + }, + }, + requestId: 'test-request-id', + }, }); const databaseUsersLink = screen.getByRole('link', { diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.tsx b/packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.tsx index e2aa91a67a8..137bc015e61 100644 --- a/packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.tsx +++ b/packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.tsx @@ -22,6 +22,7 @@ import { generateFakerMappings, mockDataGeneratorPreviousButtonClicked, } from '../../modules/collection-tab'; +import { SCHEMA_ANALYSIS_STATE_COMPLETE } from '../../schema-analysis-types'; import RawSchemaConfirmationScreen from './raw-schema-confirmation-screen'; import FakerSchemaEditorScreen from './faker-schema-editor-screen'; import ScriptScreen from './script-screen'; @@ -51,6 +52,7 @@ interface Props { onPreviousStep: () => void; namespace: string; fakerSchemaGenerationState: MockDataGeneratorState; + schemaAnalysis: CollectionState['schemaAnalysis']; } const MockDataGeneratorModal = ({ @@ -62,10 +64,18 @@ const MockDataGeneratorModal = ({ onPreviousStep, namespace, fakerSchemaGenerationState, + schemaAnalysis, }: Props) => { const [isSchemaConfirmed, setIsSchemaConfirmed] = React.useState(false); + // Extract array length map for dependency array + const arrayLengthMap = useMemo(() => { + return schemaAnalysis?.status === SCHEMA_ANALYSIS_STATE_COMPLETE + ? schemaAnalysis.arrayLengthMap + : {}; + }, [schemaAnalysis]); + const modalBodyContent = useMemo(() => { switch (currentStep) { case MockDataGeneratorStep.SCHEMA_CONFIRMATION: @@ -83,9 +93,30 @@ const MockDataGeneratorModal = ({ case MockDataGeneratorStep.PREVIEW_DATA: return <>; // TODO: CLOUDP-333857 case MockDataGeneratorStep.GENERATE_DATA: - return ; + // Only render ScriptScreen if we have completed faker schema and schema analysis + if ( + fakerSchemaGenerationState.status === 'completed' && + schemaAnalysis?.status === SCHEMA_ANALYSIS_STATE_COMPLETE + ) { + return ( + + ); + } + return
Loading script...
; } - }, [currentStep, fakerSchemaGenerationState, isSchemaConfirmed]); + }, [ + currentStep, + fakerSchemaGenerationState, + isSchemaConfirmed, + namespace, + schemaAnalysis?.status, + arrayLengthMap, + ]); const isNextButtonDisabled = currentStep === MockDataGeneratorStep.SCHEMA_EDITOR && !isSchemaConfirmed; @@ -159,6 +190,7 @@ const mapStateToProps = (state: CollectionState) => ({ currentStep: state.mockDataGenerator.currentStep, namespace: state.namespace, fakerSchemaGenerationState: state.fakerSchemaGeneration, + schemaAnalysis: state.schemaAnalysis, }); const ConnectedMockDataGeneratorModal = connect(mapStateToProps, { diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/script-screen.tsx b/packages/compass-collection/src/components/mock-data-generator-modal/script-screen.tsx index a3f090ce94f..65033eb9c6a 100644 --- a/packages/compass-collection/src/components/mock-data-generator-modal/script-screen.tsx +++ b/packages/compass-collection/src/components/mock-data-generator-modal/script-screen.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import { Body, Code, @@ -14,6 +14,9 @@ import { useDarkMode, } from '@mongodb-js/compass-components'; import { useConnectionInfo } from '@mongodb-js/compass-connections/provider'; +import { generateScript } from './script-generation-utils'; +import type { FakerSchema } from './types'; +import type { ArrayLengthMap } from './script-generation-utils'; const RUN_SCRIPT_COMMAND = ` mongosh "mongodb+srv://.mongodb.net/" \\ @@ -63,10 +66,41 @@ const resourceSectionHeader = css({ marginBottom: spacing[300], }); -const ScriptScreen = () => { +interface ScriptScreenProps { + fakerSchema: FakerSchema; + namespace: string; + arrayLengthMap?: ArrayLengthMap; + documentCount?: number; +} + +const ScriptScreen = ({ + fakerSchema, + namespace, + arrayLengthMap = {}, + documentCount = 100, +}: ScriptScreenProps) => { const isDarkMode = useDarkMode(); const connectionInfo = useConnectionInfo(); + // Parse namespace to get database and collection names + const [databaseName, collectionName] = namespace.split('.'); + + // Generate the script using the faker schema + const scriptResult = useMemo(() => { + return generateScript(fakerSchema, { + documentCount, + databaseName, + collectionName, + arrayLengthMap, + }); + }, [ + fakerSchema, + documentCount, + databaseName, + collectionName, + arrayLengthMap, + ]); + return (
@@ -100,9 +134,10 @@ const ScriptScreen = () => { In the directory that you created, create a file named mockdatascript.js (or any name you'd like). - {/* TODO: CLOUDP-333860: Hook up to the code generated as part script generation */} - TK + {scriptResult.success + ? scriptResult.script + : `// Error generating script: ${scriptResult.error}`}
@@ -157,3 +192,4 @@ const ScriptScreen = () => { }; export default ScriptScreen; +export type { ScriptScreenProps }; diff --git a/packages/compass-collection/src/modules/collection-tab.ts b/packages/compass-collection/src/modules/collection-tab.ts index 77720bd4bf3..b567c2097ff 100644 --- a/packages/compass-collection/src/modules/collection-tab.ts +++ b/packages/compass-collection/src/modules/collection-tab.ts @@ -143,6 +143,7 @@ interface SchemaAnalysisStartedAction { interface SchemaAnalysisFinishedAction { type: CollectionActions.SchemaAnalysisFinished; processedSchema: Record; + arrayLengthMap: Record; sampleDocument: Document; schemaMetadata: { maxNestingDepth: number; @@ -262,6 +263,7 @@ const reducer: Reducer = ( schemaAnalysis: { status: SCHEMA_ANALYSIS_STATE_COMPLETE, processedSchema: action.processedSchema, + arrayLengthMap: action.arrayLengthMap, sampleDocument: action.sampleDocument, schemaMetadata: action.schemaMetadata, }, @@ -629,7 +631,7 @@ export const analyzeCollectionSchema = (): CollectionThunkAction< ); // Transform schema to structure that will be used by the LLM - const processedSchema = processSchema(schema); + const processSchemaResult = processSchema(schema); const maxNestingDepth = await calculateSchemaDepth(schema); const { database, collection } = toNS(namespace); @@ -648,7 +650,8 @@ export const analyzeCollectionSchema = (): CollectionThunkAction< dispatch({ type: CollectionActions.SchemaAnalysisFinished, - processedSchema, + processedSchema: processSchemaResult.fieldInfo, + arrayLengthMap: processSchemaResult.arrayLengthMap, sampleDocument: sampleDocuments[0], schemaMetadata, }); diff --git a/packages/compass-collection/src/schema-analysis-types.ts b/packages/compass-collection/src/schema-analysis-types.ts index 954080599af..66a68bb263d 100644 --- a/packages/compass-collection/src/schema-analysis-types.ts +++ b/packages/compass-collection/src/schema-analysis-types.ts @@ -54,6 +54,7 @@ export interface FieldInfo { export type SchemaAnalysisCompletedState = { status: typeof SCHEMA_ANALYSIS_STATE_COMPLETE; processedSchema: Record; + arrayLengthMap: Record; sampleDocument: Document; schemaMetadata: { maxNestingDepth: number; From f4c8c19ece19bb55e22ce62a1479bec7de2919f5 Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Mon, 22 Sep 2025 17:49:33 -0400 Subject: [PATCH 21/40] Fix merge conflicts --- .../mock-data-generator-modal.spec.tsx | 1 - .../mock-data-generator-modal/script-generation-utils.ts | 2 -- 2 files changed, 3 deletions(-) diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.spec.tsx b/packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.spec.tsx index 80059e34a60..21ef9bdb9bf 100644 --- a/packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.spec.tsx +++ b/packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.spec.tsx @@ -6,7 +6,6 @@ import { waitFor, userEvent, waitForElementToBeRemoved, - waitForElementToBeRemoved, } from '@mongodb-js/testing-library-compass'; import { Provider } from 'react-redux'; import { createStore, applyMiddleware } from 'redux'; diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.ts b/packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.ts index 8188b792af5..029b703c828 100644 --- a/packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.ts +++ b/packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.ts @@ -1,7 +1,5 @@ import type { MongoDBFieldType } from '@mongodb-js/compass-generative-ai'; import type { FakerFieldMapping } from './types'; -import type { MongoDBFieldType } from '@mongodb-js/compass-generative-ai'; -import type { FakerFieldMapping } from './types'; export type FakerArg = string | number | boolean | { json: string }; From 05745e34ad736aa79347349f09c6d1c65c512e20 Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Wed, 24 Sep 2025 10:47:40 -0400 Subject: [PATCH 22/40] DB and collection name parsing --- .../mock-data-generator-modal/script-screen.tsx | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/script-screen.tsx b/packages/compass-collection/src/components/mock-data-generator-modal/script-screen.tsx index 65033eb9c6a..2a926cc2754 100644 --- a/packages/compass-collection/src/components/mock-data-generator-modal/script-screen.tsx +++ b/packages/compass-collection/src/components/mock-data-generator-modal/script-screen.tsx @@ -14,6 +14,7 @@ import { useDarkMode, } from '@mongodb-js/compass-components'; import { useConnectionInfo } from '@mongodb-js/compass-connections/provider'; +import toNS from 'mongodb-ns'; import { generateScript } from './script-generation-utils'; import type { FakerSchema } from './types'; import type { ArrayLengthMap } from './script-generation-utils'; @@ -82,24 +83,18 @@ const ScriptScreen = ({ const isDarkMode = useDarkMode(); const connectionInfo = useConnectionInfo(); - // Parse namespace to get database and collection names - const [databaseName, collectionName] = namespace.split('.'); + // Parse namespace to get database and collection names using robust toNS utility + const { database, collection } = toNS(namespace); // Generate the script using the faker schema const scriptResult = useMemo(() => { return generateScript(fakerSchema, { documentCount, - databaseName, - collectionName, + databaseName: database, + collectionName: collection, arrayLengthMap, }); - }, [ - fakerSchema, - documentCount, - databaseName, - collectionName, - arrayLengthMap, - ]); + }, [fakerSchema, documentCount, database, collection, arrayLengthMap]); return (
From 4ec9417ef3d3357dd812d4a78ed81b52b08327df Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Wed, 24 Sep 2025 10:47:57 -0400 Subject: [PATCH 23/40] WIP --- .../src/components/mock-data-generator-modal/script-screen.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/script-screen.tsx b/packages/compass-collection/src/components/mock-data-generator-modal/script-screen.tsx index 2a926cc2754..7e6a90c827b 100644 --- a/packages/compass-collection/src/components/mock-data-generator-modal/script-screen.tsx +++ b/packages/compass-collection/src/components/mock-data-generator-modal/script-screen.tsx @@ -83,7 +83,6 @@ const ScriptScreen = ({ const isDarkMode = useDarkMode(); const connectionInfo = useConnectionInfo(); - // Parse namespace to get database and collection names using robust toNS utility const { database, collection } = toNS(namespace); // Generate the script using the faker schema From 52af9b41ab1268defcc95f4a426b1fe9167898cc Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Wed, 24 Sep 2025 10:55:05 -0400 Subject: [PATCH 24/40] WIP --- .../mock-data-generator-modal.tsx | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.tsx b/packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.tsx index 137bc015e61..be02d7d819c 100644 --- a/packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.tsx +++ b/packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.tsx @@ -69,7 +69,6 @@ const MockDataGeneratorModal = ({ const [isSchemaConfirmed, setIsSchemaConfirmed] = React.useState(false); - // Extract array length map for dependency array const arrayLengthMap = useMemo(() => { return schemaAnalysis?.status === SCHEMA_ANALYSIS_STATE_COMPLETE ? schemaAnalysis.arrayLengthMap @@ -93,20 +92,18 @@ const MockDataGeneratorModal = ({ case MockDataGeneratorStep.PREVIEW_DATA: return <>; // TODO: CLOUDP-333857 case MockDataGeneratorStep.GENERATE_DATA: - // Only render ScriptScreen if we have completed faker schema and schema analysis - if ( - fakerSchemaGenerationState.status === 'completed' && - schemaAnalysis?.status === SCHEMA_ANALYSIS_STATE_COMPLETE - ) { + // UX flow ensures faker schema is completed, but TypeScript needs the guard + if (fakerSchemaGenerationState.status === 'completed') { return ( ); } + // Should never happen, but needed for TypeScript guard return
Loading script...
; } }, [ @@ -114,7 +111,6 @@ const MockDataGeneratorModal = ({ fakerSchemaGenerationState, isSchemaConfirmed, namespace, - schemaAnalysis?.status, arrayLengthMap, ]); From c6512f0a816ed3753320eed69d90439d24306485 Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Wed, 24 Sep 2025 10:58:49 -0400 Subject: [PATCH 25/40] Comments --- .../mock-data-generator-modal/script-generation-utils.ts | 5 +++++ .../components/mock-data-generator-modal/script-screen.tsx | 4 +++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.ts b/packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.ts index 029b703c828..27d70e10e4c 100644 --- a/packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.ts +++ b/packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.ts @@ -6,6 +6,11 @@ export type FakerArg = string | number | boolean | { json: string }; const DEFAULT_ARRAY_LENGTH = 3; const INDENT_SIZE = 2; +// Stores the average array length of each array. +// Examples: +// "users[]": 5 - users array has 5 elements +// "users[].posts[]": 3 - each user has 3 posts +// "matrix[]": 3, "matrix[][]": 4 - matrix has 3 rows, each row has 4 columns export type ArrayLengthMap = Record; export interface ScriptOptions { diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/script-screen.tsx b/packages/compass-collection/src/components/mock-data-generator-modal/script-screen.tsx index 7e6a90c827b..25d4b3dcfaa 100644 --- a/packages/compass-collection/src/components/mock-data-generator-modal/script-screen.tsx +++ b/packages/compass-collection/src/components/mock-data-generator-modal/script-screen.tsx @@ -26,6 +26,8 @@ mongosh "mongodb+srv://.mongodb.net/" \\ mockdatascript.js `; +const DEFAULT_DOCUMENT_COUNT = 100; + const outerSectionStyles = css({ display: 'flex', flexDirection: 'column', @@ -78,7 +80,7 @@ const ScriptScreen = ({ fakerSchema, namespace, arrayLengthMap = {}, - documentCount = 100, + documentCount = DEFAULT_DOCUMENT_COUNT, }: ScriptScreenProps) => { const isDarkMode = useDarkMode(); const connectionInfo = useConnectionInfo(); From 2d1075cb4a6d6f61301250d4959013c162b2bf87 Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Wed, 24 Sep 2025 12:52:41 -0400 Subject: [PATCH 26/40] Error banner --- .../mock-data-generator-modal.spec.tsx | 73 ++++++++++++++++++- .../script-screen.tsx | 13 +++- 2 files changed, 83 insertions(+), 3 deletions(-) diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.spec.tsx b/packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.spec.tsx index 21ef9bdb9bf..f51d33fef2d 100644 --- a/packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.spec.tsx +++ b/packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.spec.tsx @@ -1,4 +1,5 @@ import { expect } from 'chai'; +import sinon from 'sinon'; import React from 'react'; import { screen, @@ -18,6 +19,7 @@ import { default as collectionTabReducer } from '../../modules/collection-tab'; import type { ConnectionInfo } from '@mongodb-js/connection-info'; import type { MockDataSchemaResponse } from '@mongodb-js/compass-generative-ai'; import type { SchemaAnalysisState } from '../../schema-analysis-types'; +import * as scriptGenerationUtils from './script-generation-utils'; const defaultSchemaAnalysisState: SchemaAnalysisState = { status: 'complete', @@ -765,7 +767,76 @@ describe('MockDataGeneratorModal', () => { .to.not.exist; }); - // todo: assert that the generated script is displayed in the code block (CLOUDP-333860) + it('shows error banner when script generation fails', async () => { + // Mock the generateScript function to return an error + const generateScriptStub = sinon.stub( + scriptGenerationUtils, + 'generateScript' + ); + generateScriptStub.returns({ + success: false, + error: 'Test error: Invalid faker schema format', + }); + + try { + await renderModal({ + currentStep: MockDataGeneratorStep.GENERATE_DATA, + fakerSchemaGeneration: { + status: 'completed', + fakerSchema: { + name: { + fakerMethod: 'person.firstName', + fakerArgs: [], + probability: 1.0, + mongoType: 'String', + }, + }, + requestId: 'test-request-id', + }, + }); + + expect(screen.getByRole('alert')).to.exist; + expect(screen.getByText(/Script Generation Failed:/)).to.exist; + expect(screen.getByText(/Test error: Invalid faker schema format/)).to + .exist; + expect(screen.getByText(/Please go back to the start screen/)).to.exist; + + const codeBlock = screen.getByText('// Script generation failed.'); + expect(codeBlock).to.exist; + } finally { + generateScriptStub.restore(); + } + }); + + it('displays the script when generation succeeds', async () => { + await renderModal({ + currentStep: MockDataGeneratorStep.GENERATE_DATA, + fakerSchemaGeneration: { + status: 'completed', + fakerSchema: { + name: { + fakerMethod: 'person.firstName', + fakerArgs: [], + probability: 1.0, + mongoType: 'String', + }, + email: { + fakerMethod: 'internet.email', + fakerArgs: [], + probability: 1.0, + mongoType: 'String', + }, + }, + requestId: 'test-request-id', + }, + }); + + // Check that no error banner is displayed + expect(screen.queryByRole('alert')).to.not.exist; + expect(screen.queryByText('Script generation failed')).to.not.exist; + expect(screen.getByText('firstName')).to.exist; // faker method + expect(screen.getByText('insertMany')).to.exist; + }); }); describe('when rendering the modal in a specific step', () => { diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/script-screen.tsx b/packages/compass-collection/src/components/mock-data-generator-modal/script-screen.tsx index 25d4b3dcfaa..41222d65d4a 100644 --- a/packages/compass-collection/src/components/mock-data-generator-modal/script-screen.tsx +++ b/packages/compass-collection/src/components/mock-data-generator-modal/script-screen.tsx @@ -1,5 +1,6 @@ import React, { useMemo } from 'react'; import { + Banner, Body, Code, Copyable, @@ -130,10 +131,18 @@ const ScriptScreen = ({ In the directory that you created, create a file named mockdatascript.js (or any name you'd like). - + {!scriptResult.success && ( + + Script Generation Failed: {scriptResult.error} +
+ Please go back to the start screen to re-submit the collection + schema. +
+ )} + {scriptResult.success ? scriptResult.script - : `// Error generating script: ${scriptResult.error}`} + : '// Script generation failed.'}
From 35efd8d3681f267b06a2790bc46000a98c7f56b6 Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Wed, 24 Sep 2025 13:12:51 -0400 Subject: [PATCH 27/40] Code component scrollable --- .../components/mock-data-generator-modal/script-screen.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/script-screen.tsx b/packages/compass-collection/src/components/mock-data-generator-modal/script-screen.tsx index 41222d65d4a..eaed25a2257 100644 --- a/packages/compass-collection/src/components/mock-data-generator-modal/script-screen.tsx +++ b/packages/compass-collection/src/components/mock-data-generator-modal/script-screen.tsx @@ -139,7 +139,11 @@ const ScriptScreen = ({ schema. )} - + {scriptResult.success ? scriptResult.script : '// Script generation failed.'} From 0142677e8728c05a4da2d5271116132c7b7fd2f9 Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Thu, 25 Sep 2025 12:14:31 -0400 Subject: [PATCH 28/40] Convert to use prettier --- .../script-generation-utils.spec.ts | 172 ++++++++++-------- .../script-generation-utils.ts | 41 ++--- 2 files changed, 113 insertions(+), 100 deletions(-) diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.spec.ts b/packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.spec.ts index 07f013e9fab..97a85e37599 100644 --- a/packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.spec.ts +++ b/packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.spec.ts @@ -94,10 +94,9 @@ describe('Script Generation', () => { expect(result.success).to.equal(true); if (result.success) { - const expectedReturnBlock = `return { - tags: Array.from({length: 3}, () => faker.lorem.word()) - };`; - expect(result.script).to.contain(expectedReturnBlock); + expect(result.script).to.contain('Array.from'); + expect(result.script).to.contain('length: 3'); + expect(result.script).to.contain('faker.lorem.word()'); // Test that the generated document code is executable const document = testDocumentCodeExecution(result.script); @@ -122,9 +121,8 @@ describe('Script Generation', () => { expect(result.success).to.equal(true); if (result.success) { - // Should generate the complete return block with proper structure const expectedReturnBlock = `return { - users: Array.from({length: 3}, () => ({ + users: Array.from({ length: 3 }, () => ({ name: faker.person.fullName(), email: faker.internet.email() })) @@ -157,7 +155,11 @@ describe('Script Generation', () => { expect(result.success).to.equal(true); if (result.success) { const expectedReturnBlock = `return { - matrix: Array.from({length: 3}, () => Array.from({length: 3}, () => faker.number.int())) + matrix: Array.from({ length: 3 }, () => + Array.from({ length: 3 }, () => + faker.number.int() + ) + ) };`; expect(result.script).to.contain(expectedReturnBlock); @@ -186,9 +188,11 @@ describe('Script Generation', () => { expect(result.success).to.equal(true); if (result.success) { const expectedReturnBlock = `return { - users: Array.from({length: 3}, () => ({ + users: Array.from({ length: 3 }, () => ({ name: faker.person.fullName(), - tags: Array.from({length: 3}, () => faker.lorem.word()) + tags: Array.from({ length: 3 }, () => + faker.lorem.word() + ) })) };`; expect(result.script).to.contain(expectedReturnBlock); @@ -224,9 +228,11 @@ describe('Script Generation', () => { if (result.success) { const expectedReturnBlock = `return { title: faker.lorem.sentence(), - authors: Array.from({length: 3}, () => ({ + authors: Array.from({ length: 3 }, () => ({ name: faker.person.fullName(), - books: Array.from({length: 3}, () => faker.lorem.words()) + books: Array.from({ length: 3 }, () => + faker.lorem.words() + ) })), publishedYear: faker.date.recent() };`; @@ -282,10 +288,7 @@ describe('Script Generation', () => { expect(result.success).to.equal(true); if (result.success) { - const expectedReturnBlock = `return { - value: faker.number.int() - };`; - expect(result.script).to.contain(expectedReturnBlock); + expect(result.script).to.contain('faker.number.int()'); // Test that the generated document code is executable const document = testDocumentCodeExecution(result.script); @@ -346,13 +349,10 @@ describe('Script Generation', () => { expect(result.success).to.equal(true); if (result.success) { - // These should be treated as regular field names, not arrays - const expectedReturnBlock = `return { - "squareBrackets[]InMiddle": faker.lorem.word(), - "field[]WithMore": faker.lorem.word(), - "start[]middle[]end": faker.lorem.word() - };`; - expect(result.script).to.contain(expectedReturnBlock); + // Verify these are treated as regular field names, not arrays + expect(result.script).to.contain('"squareBrackets[]InMiddle"'); + expect(result.script).to.contain('"field[]WithMore"'); + expect(result.script).to.contain('"start[]middle[]end"'); expect(result.script).not.to.contain('Array.from'); // Test that the generated document code is executable @@ -385,6 +385,9 @@ describe('Script Generation', () => { // Should not contain unescaped special characters that could break JS expect(result.script).not.to.contain("use('test'db"); expect(result.script).not.to.contain("getCollection('coll\nwith"); + + // Test that the generated document code is executable + testDocumentCodeExecution(result.script); } }); }); @@ -403,10 +406,8 @@ describe('Script Generation', () => { expect(result.success).to.equal(true); if (result.success) { - const expectedReturnBlock = `return { - tags: Array.from({length: 3}, () => faker.lorem.word()) - };`; - expect(result.script).to.contain(expectedReturnBlock); + expect(result.script).to.contain('length: 3'); + expect(result.script).to.contain('faker.lorem.word()'); // Test that the generated document code is executable const document = testDocumentCodeExecution(result.script); @@ -432,10 +433,8 @@ describe('Script Generation', () => { expect(result.success).to.equal(true); if (result.success) { - const expectedReturnBlock = `return { - tags: Array.from({length: 5}, () => faker.lorem.word()) - };`; - expect(result.script).to.contain(expectedReturnBlock); + expect(result.script).to.contain('length: 5'); + expect(result.script).to.contain('faker.lorem.word()'); // Test that the generated document code is executable const document = testDocumentCodeExecution(result.script); @@ -463,8 +462,10 @@ describe('Script Generation', () => { expect(result.success).to.equal(true); if (result.success) { const expectedReturnBlock = `return { - users: Array.from({length: 5}, () => ({ - tags: Array.from({length: 4}, () => faker.lorem.word()) + users: Array.from({ length: 5 }, () => ({ + tags: Array.from({ length: 4 }, () => + faker.lorem.word() + ) })) };`; expect(result.script).to.contain(expectedReturnBlock); @@ -498,10 +499,13 @@ describe('Script Generation', () => { expect(result.success).to.equal(true); if (result.success) { - // Should have tags array with length 0 (empty array) and categories with length 2 const expectedReturnBlock = `return { - tags: Array.from({length: 0}, () => faker.lorem.word()), - categories: Array.from({length: 2}, () => faker.lorem.word()) + tags: Array.from({ length: 0 }, () => + faker.lorem.word() + ), + categories: Array.from({ length: 2 }, () => + faker.lorem.word() + ) };`; expect(result.script).to.contain(expectedReturnBlock); @@ -537,8 +541,18 @@ describe('Script Generation', () => { expect(result.success).to.equal(true); if (result.success) { const expectedReturnBlock = `return { - matrix: Array.from({length: 2}, () => Array.from({length: 5}, () => faker.number.int())), - cube: Array.from({length: 3}, () => Array.from({length: 4}, () => Array.from({length: 2}, () => faker.number.float()))) + matrix: Array.from({ length: 2 }, () => + Array.from({ length: 5 }, () => + faker.number.int() + ) + ), + cube: Array.from({ length: 3 }, () => + Array.from({ length: 4 }, () => + Array.from({ length: 2 }, () => + faker.number.float() + ) + ) + ) };`; expect(result.script).to.contain(expectedReturnBlock); @@ -572,17 +586,24 @@ describe('Script Generation', () => { expect(result.success).to.equal(true); if (result.success) { - // Complex nested structure with custom array lengths const expectedReturnBlock = `return { - users: Array.from({length: 2}, () => ({ + users: Array.from({ length: 2 }, () => ({ name: faker.person.fullName(), - tags: Array.from({length: 3}, () => faker.lorem.word()), - posts: Array.from({length: 4}, () => ({ + tags: Array.from({ length: 3 }, () => + faker.lorem.word() + ), + posts: Array.from({ length: 4 }, () => ({ title: faker.lorem.sentence(), - comments: Array.from({length: 5}, () => faker.lorem.words()) + comments: Array.from({ length: 5 }, () => + faker.lorem.words() + ) })) })), - matrix: Array.from({length: 2}, () => Array.from({length: 3}, () => faker.number.int())) + matrix: Array.from({ length: 2 }, () => + Array.from({ length: 3 }, () => + faker.number.int() + ) + ) };`; expect(result.script).to.contain(expectedReturnBlock); @@ -614,19 +635,18 @@ describe('Script Generation', () => { expect(result.success).to.equal(true); if (result.success) { // Verify field names with [] in middle are treated as regular field names - expect(result.script).to.contain( - '"brackets[]InMiddle": faker.lorem.word()' - ); + expect(result.script).to.contain('"brackets[]InMiddle"'); + expect(result.script).to.contain('faker.lorem.word()'); // Verify array of objects with bracket field names containing arrays - expect(result.script).to.contain( - 'items: Array.from({length: 2}, () => ({\n "nested[]ArrayFieldWithBrackets": Array.from({length: 3}, () => faker.lorem.sentence())' - ); + expect(result.script).to.contain('"nested[]ArrayFieldWithBrackets"'); + expect(result.script).to.contain('Array.from({ length: 3 }'); + expect(result.script).to.contain('faker.lorem.sentence()'); // Verify multi-dimensional arrays with bracket field names - expect(result.script).to.contain( - '"matrix[]WithBrackets": Array.from({length: 2}, () => Array.from({length: 4}, () => faker.number.int()))' - ); + expect(result.script).to.contain('"matrix[]WithBrackets"'); + expect(result.script).to.contain('Array.from({ length: 2 }'); + expect(result.script).to.contain('faker.number.int()'); // Test that the generated document code is executable const document = testDocumentCodeExecution(result.script); @@ -673,10 +693,7 @@ describe('Script Generation', () => { expect(result.success).to.equal(true); if (result.success) { - const expectedReturnBlock = `return { - unknownField: faker.lorem.word() - };`; - expect(result.script).to.contain(expectedReturnBlock); + expect(result.script).to.contain('faker.lorem.word()'); // Test that the generated document code is executable const document = testDocumentCodeExecution(result.script); @@ -997,9 +1014,9 @@ describe('Script Generation', () => { expect(result.success).to.equal(true); if (result.success) { - expect(result.script).to.contain( - 'faker.number.int({"min":0,"max":100})' - ); + expect(result.script).to.contain('faker.number.int('); + expect(result.script).to.contain('min: 0'); + expect(result.script).to.contain('max: 100'); // Test that the generated document code is executable testDocumentCodeExecution(result.script); @@ -1023,9 +1040,10 @@ describe('Script Generation', () => { expect(result.success).to.equal(true); if (result.success) { - expect(result.script).to.contain( - "faker.helpers.arrayElement(['red', 'blue', 'green'])" - ); + expect(result.script).to.contain('faker.helpers.arrayElement('); + expect(result.script).to.contain('red'); + expect(result.script).to.contain('blue'); + expect(result.script).to.contain('green'); // Test that the generated document code is executable testDocumentCodeExecution(result.script); @@ -1075,9 +1093,9 @@ describe('Script Generation', () => { expect(result.success).to.equal(true); if (result.success) { - expect(result.script).to.contain( - 'faker.helpers.arrayElement(["It\'s a \'test\' string", "another option"])' - ); + expect(result.script).to.contain('faker.helpers.arrayElement('); + expect(result.script).to.contain("It's a 'test' string"); + expect(result.script).to.contain('another option'); // Test that the generated document code is executable testDocumentCodeExecution(result.script); @@ -1210,7 +1228,9 @@ describe('Script Generation', () => { expect(result.success).to.equal(true); if (result.success) { const expectedReturnBlock = `return { - ...(Math.random() < 0.7 ? { optionalField: faker.lorem.word() } : {}) + ...(Math.random() < 0.7 + ? { optionalField: faker.lorem.word() } + : {}) };`; expect(result.script).to.contain(expectedReturnBlock); @@ -1237,8 +1257,14 @@ describe('Script Generation', () => { if (result.success) { const expectedReturnBlock = `return { alwaysPresent: faker.person.fullName(), - ...(Math.random() < 0.8 ? { sometimesPresent: faker.internet.email() } : {}), - ...(Math.random() < 0.2 ? { rarelyPresent: faker.phone.number() } : {}), + ...(Math.random() < 0.8 + ? { + sometimesPresent: faker.internet.email() + } + : {}), + ...(Math.random() < 0.2 + ? { rarelyPresent: faker.phone.number() } + : {}), defaultProbability: faker.lorem.word() };`; expect(result.script).to.contain(expectedReturnBlock); @@ -1272,9 +1298,8 @@ describe('Script Generation', () => { expect(result.success).to.equal(true); if (result.success) { - expect(result.script).to.contain( - '...(Math.random() < 0.9 ? { conditionalAge: faker.number.int(18, 65) } : {})' - ); + expect(result.script).to.contain('Math.random() < 0.9'); + expect(result.script).to.contain('faker.number.int(18, 65)'); // Test that the generated document code is executable testDocumentCodeExecution(result.script); @@ -1299,9 +1324,8 @@ describe('Script Generation', () => { expect(result.success).to.equal(true); if (result.success) { - expect(result.script).to.contain( - '...(Math.random() < 0.5 ? { unknownField: faker.lorem.word() } : {})' - ); + expect(result.script).to.contain('Math.random() < 0.5'); + expect(result.script).to.contain('faker.lorem.word()'); // Test that the generated document code is executable testDocumentCodeExecution(result.script); diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.ts b/packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.ts index 27d70e10e4c..5c84a2c915a 100644 --- a/packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.ts +++ b/packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.ts @@ -1,10 +1,10 @@ import type { MongoDBFieldType } from '@mongodb-js/compass-generative-ai'; import type { FakerFieldMapping } from './types'; +import { prettify } from '@mongodb-js/compass-editor'; export type FakerArg = string | number | boolean | { json: string }; const DEFAULT_ARRAY_LENGTH = 3; -const INDENT_SIZE = 2; // Stores the average array length of each array. // Examples: @@ -48,11 +48,11 @@ export function generateScript( const documentCode = renderDocumentCode( structure, - INDENT_SIZE * 2, // 4 spaces: 2 for function body + 2 for inside return statement options.arrayLengthMap || {} ); - const script = `// Mock Data Generator Script + // Generate unformatted script + const unformattedScript = `// Mock Data Generator Script // Generated for collection: ${JSON.stringify( options.databaseName )}.${JSON.stringify(options.collectionName)} @@ -65,13 +65,13 @@ use(${JSON.stringify(options.databaseName)}); // Document generation function function generateDocument() { - return ${documentCode}; +return ${documentCode}; } // Generate and insert documents const documents = []; for (let i = 0; i < ${options.documentCount}; i++) { - documents.push(generateDocument()); +documents.push(generateDocument()); } // Insert documents into collection @@ -79,9 +79,12 @@ db.getCollection(${JSON.stringify( options.collectionName )}).insertMany(documents); -console.log(\`Successfully inserted \${documents.length} documents into ${JSON.stringify( +console.log("Successfully inserted " + documents.length + " documents into " + ${JSON.stringify( options.databaseName - )}.${JSON.stringify(options.collectionName)}\`);`; + )} + "." + ${JSON.stringify(options.collectionName)});`; + + // Format the script using prettier + const script = prettify(unformattedScript, 'javascript'); return { script, @@ -306,7 +309,6 @@ function insertIntoStructure( */ function renderDocumentCode( structure: DocumentStructure, - indent: number = INDENT_SIZE, arrayLengthMap: ArrayLengthMap = {}, currentPath: string = '' ): string { @@ -315,8 +317,6 @@ function renderDocumentCode( // - If DocumentStructure: generate nested object // - If ArrayStructure: generate array - const fieldIndent = ' '.repeat(indent); - const closingBraceIndent = ' '.repeat(indent - INDENT_SIZE); const documentFields: string[] = []; for (const [fieldName, value] of Object.entries(structure)) { @@ -338,15 +338,13 @@ function renderDocumentCode( if (probability < 1.0) { // Use Math.random for conditional field inclusion documentFields.push( - `${fieldIndent}...(Math.random() < ${probability} ? { ${formatFieldName( + `...(Math.random() < ${probability} ? { ${formatFieldName( fieldName )}: ${fakerCall} } : {})` ); } else { // Normal field inclusion - documentFields.push( - `${fieldIndent}${formatFieldName(fieldName)}: ${fakerCall}` - ); + documentFields.push(`${formatFieldName(fieldName)}: ${fakerCall}`); } } else if ('type' in value && value.type === 'array') { // It's an array @@ -355,14 +353,11 @@ function renderDocumentCode( : `${fieldName}[]`; const arrayCode = renderArrayCode( value as ArrayStructure, - indent + INDENT_SIZE, fieldName, arrayLengthMap, fieldPath ); - documentFields.push( - `${fieldIndent}${formatFieldName(fieldName)}: ${arrayCode}` - ); + documentFields.push(`${formatFieldName(fieldName)}: ${arrayCode}`); } else { // It's a nested object: recursive call @@ -372,13 +367,10 @@ function renderDocumentCode( const nestedCode = renderDocumentCode( value as DocumentStructure, - indent + INDENT_SIZE, arrayLengthMap, nestedPath ); - documentFields.push( - `${fieldIndent}${formatFieldName(fieldName)}: ${nestedCode}` - ); + documentFields.push(`${formatFieldName(fieldName)}: ${nestedCode}`); } } @@ -387,7 +379,7 @@ function renderDocumentCode( return '{}'; } - return `{\n${documentFields.join(',\n')}\n${closingBraceIndent}}`; + return `{${documentFields.join(',')}}`; } /** @@ -411,7 +403,6 @@ function formatFieldName(fieldName: string): string { */ function renderArrayCode( arrayStructure: ArrayStructure, - indent: number = INDENT_SIZE, fieldName: string = '', arrayLengthMap: ArrayLengthMap = {}, currentFieldPath: string = '' @@ -433,7 +424,6 @@ function renderArrayCode( const fieldPath = currentFieldPath + '[]'; const nestedArrayCode = renderArrayCode( elementType as ArrayStructure, - indent, fieldName, arrayLengthMap, fieldPath @@ -442,7 +432,6 @@ function renderArrayCode( } else { const objectCode = renderDocumentCode( elementType as DocumentStructure, - indent, arrayLengthMap, currentFieldPath ); From 6e36bdeac9ca0c9b8babc73f7d3aedf6366e1ab5 Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Thu, 25 Sep 2025 12:31:23 -0400 Subject: [PATCH 29/40] Connect redux directly --- .../mock-data-generator-modal.tsx | 33 ++------------ .../script-screen.tsx | 45 +++++++++++++++---- 2 files changed, 40 insertions(+), 38 deletions(-) diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.tsx b/packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.tsx index be02d7d819c..5e9772d773d 100644 --- a/packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.tsx +++ b/packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.tsx @@ -22,7 +22,7 @@ import { generateFakerMappings, mockDataGeneratorPreviousButtonClicked, } from '../../modules/collection-tab'; -import { SCHEMA_ANALYSIS_STATE_COMPLETE } from '../../schema-analysis-types'; + import RawSchemaConfirmationScreen from './raw-schema-confirmation-screen'; import FakerSchemaEditorScreen from './faker-schema-editor-screen'; import ScriptScreen from './script-screen'; @@ -52,7 +52,6 @@ interface Props { onPreviousStep: () => void; namespace: string; fakerSchemaGenerationState: MockDataGeneratorState; - schemaAnalysis: CollectionState['schemaAnalysis']; } const MockDataGeneratorModal = ({ @@ -64,17 +63,10 @@ const MockDataGeneratorModal = ({ onPreviousStep, namespace, fakerSchemaGenerationState, - schemaAnalysis, }: Props) => { const [isSchemaConfirmed, setIsSchemaConfirmed] = React.useState(false); - const arrayLengthMap = useMemo(() => { - return schemaAnalysis?.status === SCHEMA_ANALYSIS_STATE_COMPLETE - ? schemaAnalysis.arrayLengthMap - : {}; - }, [schemaAnalysis]); - const modalBodyContent = useMemo(() => { switch (currentStep) { case MockDataGeneratorStep.SCHEMA_CONFIRMATION: @@ -92,27 +84,9 @@ const MockDataGeneratorModal = ({ case MockDataGeneratorStep.PREVIEW_DATA: return <>; // TODO: CLOUDP-333857 case MockDataGeneratorStep.GENERATE_DATA: - // UX flow ensures faker schema is completed, but TypeScript needs the guard - if (fakerSchemaGenerationState.status === 'completed') { - return ( - - ); - } - // Should never happen, but needed for TypeScript guard - return
Loading script...
; + return ; } - }, [ - currentStep, - fakerSchemaGenerationState, - isSchemaConfirmed, - namespace, - arrayLengthMap, - ]); + }, [currentStep, fakerSchemaGenerationState, isSchemaConfirmed]); const isNextButtonDisabled = currentStep === MockDataGeneratorStep.SCHEMA_EDITOR && !isSchemaConfirmed; @@ -186,7 +160,6 @@ const mapStateToProps = (state: CollectionState) => ({ currentStep: state.mockDataGenerator.currentStep, namespace: state.namespace, fakerSchemaGenerationState: state.fakerSchemaGeneration, - schemaAnalysis: state.schemaAnalysis, }); const ConnectedMockDataGeneratorModal = connect(mapStateToProps, { diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/script-screen.tsx b/packages/compass-collection/src/components/mock-data-generator-modal/script-screen.tsx index eaed25a2257..4e567d9c4c5 100644 --- a/packages/compass-collection/src/components/mock-data-generator-modal/script-screen.tsx +++ b/packages/compass-collection/src/components/mock-data-generator-modal/script-screen.tsx @@ -1,4 +1,5 @@ import React, { useMemo } from 'react'; +import { connect } from 'react-redux'; import { Banner, Body, @@ -19,6 +20,8 @@ import toNS from 'mongodb-ns'; import { generateScript } from './script-generation-utils'; import type { FakerSchema } from './types'; import type { ArrayLengthMap } from './script-generation-utils'; +import type { CollectionState } from '../../modules/collection-tab'; +import { SCHEMA_ANALYSIS_STATE_COMPLETE } from '../../schema-analysis-types'; const RUN_SCRIPT_COMMAND = ` mongosh "mongodb+srv://.mongodb.net/" \\ @@ -27,8 +30,6 @@ mongosh "mongodb+srv://.mongodb.net/" \\ mockdatascript.js `; -const DEFAULT_DOCUMENT_COUNT = 100; - const outerSectionStyles = css({ display: 'flex', flexDirection: 'column', @@ -71,17 +72,17 @@ const resourceSectionHeader = css({ }); interface ScriptScreenProps { - fakerSchema: FakerSchema; + fakerSchema: FakerSchema | null; namespace: string; - arrayLengthMap?: ArrayLengthMap; - documentCount?: number; + arrayLengthMap: ArrayLengthMap; + documentCount: number; } const ScriptScreen = ({ fakerSchema, namespace, - arrayLengthMap = {}, - documentCount = DEFAULT_DOCUMENT_COUNT, + arrayLengthMap, + documentCount, }: ScriptScreenProps) => { const isDarkMode = useDarkMode(); const connectionInfo = useConnectionInfo(); @@ -90,6 +91,14 @@ const ScriptScreen = ({ // Generate the script using the faker schema const scriptResult = useMemo(() => { + // Handle case where fakerSchema is not yet available + if (!fakerSchema) { + return { + success: false as const, + error: 'Faker schema not available', + }; + } + return generateScript(fakerSchema, { documentCount, databaseName: database, @@ -200,5 +209,25 @@ const ScriptScreen = ({ ); }; -export default ScriptScreen; +const mapStateToProps = (state: CollectionState) => { + const { fakerSchemaGeneration, namespace, schemaAnalysis } = state; + + return { + fakerSchema: + fakerSchemaGeneration.status === 'completed' + ? fakerSchemaGeneration.fakerSchema + : null, + namespace, + arrayLengthMap: + schemaAnalysis?.status === SCHEMA_ANALYSIS_STATE_COMPLETE + ? schemaAnalysis.arrayLengthMap + : {}, + // TODO(CLOUDP-333856): When document count step is implemented, get documentCount from state + documentCount: 100, + }; +}; + +const ConnectedScriptScreen = connect(mapStateToProps)(ScriptScreen); + +export default ConnectedScriptScreen; export type { ScriptScreenProps }; From 6bacb485c4d83da6ad9a0a81cb13c61ab74fd8ba Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Thu, 25 Sep 2025 12:33:07 -0400 Subject: [PATCH 30/40] Style --- .../components/mock-data-generator-modal/script-screen.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/script-screen.tsx b/packages/compass-collection/src/components/mock-data-generator-modal/script-screen.tsx index 4e567d9c4c5..d2d24aaac2c 100644 --- a/packages/compass-collection/src/components/mock-data-generator-modal/script-screen.tsx +++ b/packages/compass-collection/src/components/mock-data-generator-modal/script-screen.tsx @@ -71,6 +71,11 @@ const resourceSectionHeader = css({ marginBottom: spacing[300], }); +const scriptCodeBlockStyles = css({ + maxHeight: '230px', + overflowY: 'auto', +}); + interface ScriptScreenProps { fakerSchema: FakerSchema | null; namespace: string; @@ -151,7 +156,7 @@ const ScriptScreen = ({ {scriptResult.success ? scriptResult.script From d2cd422dc3c98789ded127713591a4799c33b8f7 Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Thu, 25 Sep 2025 13:04:04 -0400 Subject: [PATCH 31/40] Error banner spacing --- .../mock-data-generator-modal/script-screen.tsx | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/script-screen.tsx b/packages/compass-collection/src/components/mock-data-generator-modal/script-screen.tsx index d2d24aaac2c..99666568f23 100644 --- a/packages/compass-collection/src/components/mock-data-generator-modal/script-screen.tsx +++ b/packages/compass-collection/src/components/mock-data-generator-modal/script-screen.tsx @@ -114,6 +114,13 @@ const ScriptScreen = ({ return (
+ {true && ( + + Script Generation Failed: ERROR +
+ Please go back to the start screen to re-submit the collection schema. +
+ )}
Prerequisites @@ -145,14 +152,6 @@ const ScriptScreen = ({ In the directory that you created, create a file named mockdatascript.js (or any name you'd like). - {!scriptResult.success && ( - - Script Generation Failed: {scriptResult.error} -
- Please go back to the start screen to re-submit the collection - schema. -
- )} Date: Thu, 25 Sep 2025 14:22:10 -0400 Subject: [PATCH 32/40] Escape chars in collection and db names --- .../script-generation-utils.spec.ts | 121 +++++++++++++++--- .../script-generation-utils.ts | 14 +- 2 files changed, 113 insertions(+), 22 deletions(-) diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.spec.ts b/packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.spec.ts index 97a85e37599..361366fcec2 100644 --- a/packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.spec.ts +++ b/packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.spec.ts @@ -369,22 +369,118 @@ describe('Script Generation', () => { name: createFieldMapping('person.fullName'), }; - const result = generateScript(schema, { + // Test various special characters: quotes, newlines, tabs + const result1 = generateScript(schema, { databaseName: 'test\'db`with"quotes', collectionName: 'coll\nwith\ttabs', documentCount: 1, }); + expect(result1.success).to.equal(true); + if (result1.success) { + expect(result1.script).to.contain('use("test\'db`with\\"quotes")'); + expect(result1.script).to.contain( + 'getCollection("coll\\nwith\\ttabs")' + ); + // Should not contain unescaped special characters that could break JS + expect(result1.script).not.to.contain("use('test'db"); + expect(result1.script).not.to.contain("getCollection('coll\nwith"); + + // Test that the generated document code is executable + testDocumentCodeExecution(result1.script); + } + + // Test backticks and dollar signs (template literal characters) + const result2 = generateScript(schema, { + databaseName: 'test`${}', + collectionName: 'collection`${}', + documentCount: 1, + }); + + expect(result2.success).to.equal(true); + if (result2.success) { + // Verify the script is syntactically valid + // eslint-disable-next-line @typescript-eslint/no-implied-eval + expect(() => new Function(result2.script)).to.not.throw(); + + // Verify template literal characters are properly escaped in console.log + expect(result2.script).to.contain('test\\`\\${}'); + expect(result2.script).to.contain('collection\\`\\${}'); + + // Test that the generated document code is executable + testDocumentCodeExecution(result2.script); + } + }); + + it('should prevent code injection attacks via database and collection names', () => { + const schema = { + name: { + mongoType: 'String' as const, + fakerMethod: 'person.firstName', + fakerArgs: [], + }, + }; + + // Test with potentially dangerous names that could inject malicious code + const result = generateScript(schema, { + databaseName: 'test`; require("fs").rmSync("/"); //', + collectionName: 'my "collection"', + documentCount: 1, + }); + expect(result.success).to.equal(true); if (result.success) { - // Should use JSON.stringify for safe string insertion - expect(result.script).to.contain('use("test\'db`with\\"quotes")'); + // Verify the script is syntactically valid JavaScript + // eslint-disable-next-line @typescript-eslint/no-implied-eval + expect(() => new Function(result.script)).to.not.throw(); + + // Verify malicious code is safely contained in string expect(result.script).to.contain( - 'db.getCollection("coll\\nwith\\ttabs")' + 'use(\'test`; require("fs").rmSync("/"); //\')' + ); + expect(result.script).to.contain('getCollection(\'my "collection"\')'); + + // Verify template literal injection is prevented (backticks are escaped) + expect(result.script).to.contain( + 'test\\`; require("fs").rmSync("/"); //' + ); + + // Verify malicious code in name is safely contained in code comment + expect(result.script).to.contain( + '// Generated for collection: test`; require("fs").rmSync("/"); //.my "collection"' ); - // Should not contain unescaped special characters that could break JS - expect(result.script).not.to.contain("use('test'db"); - expect(result.script).not.to.contain("getCollection('coll\nwith"); + + // Test that the generated document code is executable + testDocumentCodeExecution(result.script); + } + }); + + it('should sanitize newlines in database and collection names in comments', () => { + const schema = { + field: { + mongoType: 'String' as const, + fakerMethod: 'lorem.word', + fakerArgs: [], + }, + }; + + // Test with names containing actual newlines and carriage returns + const result = generateScript(schema, { + databaseName: 'test\nwith\nnewlines', + collectionName: 'coll\rwith\r\nreturns', + documentCount: 1, + }); + + expect(result.success).to.equal(true); + if (result.success) { + // Verify newlines are replaced with spaces in comments to prevent syntax errors + expect(result.script).to.contain( + '// Generated for collection: test with newlines.coll with returns' + ); + + // Verify the script is still syntactically valid + // eslint-disable-next-line @typescript-eslint/no-implied-eval + expect(() => new Function(result.script)).to.not.throw(); // Test that the generated document code is executable testDocumentCodeExecution(result.script); @@ -1028,7 +1124,7 @@ describe('Script Generation', () => { color: { mongoType: 'String' as const, fakerMethod: 'helpers.arrayElement', - fakerArgs: [{ json: "['red', 'blue', 'green']" }], + fakerArgs: [{ json: '["red", "blue", "green"]' }], }, }; @@ -1184,12 +1280,6 @@ describe('Script Generation', () => { fakerArgs: [], probability: -0.5, // Invalid - should default to 1.0 }, - field3: { - mongoType: 'String' as const, - fakerMethod: 'lorem.word', - fakerArgs: [], - probability: 'invalid' as any, // Invalid - should default to 1.0 - }, }; const result = generateScript(schema, { @@ -1203,8 +1293,7 @@ describe('Script Generation', () => { // All fields should be treated as probability 1.0 (always present) const expectedReturnBlock = `return { field1: faker.lorem.word(), - field2: faker.lorem.word(), - field3: faker.lorem.word() + field2: faker.lorem.word() };`; expect(result.script).to.contain(expectedReturnBlock); expect(result.script).not.to.contain('Math.random()'); diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.ts b/packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.ts index 5c84a2c915a..8e59655e297 100644 --- a/packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.ts +++ b/packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.ts @@ -53,9 +53,10 @@ export function generateScript( // Generate unformatted script const unformattedScript = `// Mock Data Generator Script -// Generated for collection: ${JSON.stringify( - options.databaseName - )}.${JSON.stringify(options.collectionName)} +// Generated for collection: ${options.databaseName.replace( + /[\r\n]/g, // Prevent newlines in names that could break the comment + ' ' + )}.${options.collectionName.replace(/[\r\n]/g, ' ')} // Document count: ${options.documentCount} const { faker } = require('@faker-js/faker'); @@ -79,9 +80,10 @@ db.getCollection(${JSON.stringify( options.collectionName )}).insertMany(documents); -console.log("Successfully inserted " + documents.length + " documents into " + ${JSON.stringify( - options.databaseName - )} + "." + ${JSON.stringify(options.collectionName)});`; +console.log(\`Successfully inserted \${documents.length} documents into ${options.databaseName.replace( + /[`$]/g, // Escape backticks and dollar signs + '\\$&' + )}.${options.collectionName.replace(/[`$]/g, '\\$&')}\`);`; // Format the script using prettier const script = prettify(unformattedScript, 'javascript'); From b4a4dc3ad342c03d1ad739e4dedd324cb67e10cb Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Thu, 25 Sep 2025 14:41:05 -0400 Subject: [PATCH 33/40] Accidental push --- .../src/components/mock-data-generator-modal/script-screen.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/script-screen.tsx b/packages/compass-collection/src/components/mock-data-generator-modal/script-screen.tsx index 99666568f23..84557f8193a 100644 --- a/packages/compass-collection/src/components/mock-data-generator-modal/script-screen.tsx +++ b/packages/compass-collection/src/components/mock-data-generator-modal/script-screen.tsx @@ -114,7 +114,7 @@ const ScriptScreen = ({ return (
- {true && ( + {!scriptResult.success && ( Script Generation Failed: ERROR
From 46169b42409d5a1dd56f14196a57804a95babab7 Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Thu, 25 Sep 2025 15:02:43 -0400 Subject: [PATCH 34/40] Fix accidental push --- .../src/components/mock-data-generator-modal/script-screen.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/script-screen.tsx b/packages/compass-collection/src/components/mock-data-generator-modal/script-screen.tsx index 84557f8193a..8b9316ae4b7 100644 --- a/packages/compass-collection/src/components/mock-data-generator-modal/script-screen.tsx +++ b/packages/compass-collection/src/components/mock-data-generator-modal/script-screen.tsx @@ -116,7 +116,7 @@ const ScriptScreen = ({
{!scriptResult.success && ( - Script Generation Failed: ERROR + Script Generation Failed: {scriptResult.error}
Please go back to the start screen to re-submit the collection schema.
From 6c9f7abf723b116b2652939c6030fcb354a31e9a Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Thu, 25 Sep 2025 15:08:29 -0400 Subject: [PATCH 35/40] Dependency for prettier --- package-lock.json | 2 ++ packages/compass-collection/package.json | 1 + 2 files changed, 3 insertions(+) diff --git a/package-lock.json b/package-lock.json index 0246bf1938d..ed3c3064bd2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48118,6 +48118,7 @@ "@mongodb-js/compass-app-stores": "^7.63.0", "@mongodb-js/compass-components": "^1.53.0", "@mongodb-js/compass-connections": "^1.77.0", + "@mongodb-js/compass-editor": "^0.55.0", "@mongodb-js/compass-generative-ai": "^0.56.0", "@mongodb-js/compass-logging": "^1.7.17", "@mongodb-js/compass-telemetry": "^1.15.3", @@ -61918,6 +61919,7 @@ "@mongodb-js/compass-app-stores": "^7.63.0", "@mongodb-js/compass-components": "^1.53.0", "@mongodb-js/compass-connections": "^1.77.0", + "@mongodb-js/compass-editor": "^0.55.0", "@mongodb-js/compass-generative-ai": "^0.56.0", "@mongodb-js/compass-logging": "^1.7.17", "@mongodb-js/compass-telemetry": "^1.15.3", diff --git a/packages/compass-collection/package.json b/packages/compass-collection/package.json index 8743a12f7e3..850847413b2 100644 --- a/packages/compass-collection/package.json +++ b/packages/compass-collection/package.json @@ -53,6 +53,7 @@ "@mongodb-js/compass-app-stores": "^7.63.0", "@mongodb-js/compass-components": "^1.53.0", "@mongodb-js/compass-connections": "^1.77.0", + "@mongodb-js/compass-editor": "^0.55.0", "@mongodb-js/compass-generative-ai": "^0.56.0", "@mongodb-js/compass-logging": "^1.7.17", "@mongodb-js/compass-telemetry": "^1.15.3", From 3cfcb0878f61aab3fe0d4fc4470db6039fc03b1b Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Mon, 29 Sep 2025 12:06:16 -0400 Subject: [PATCH 36/40] Merge conflict --- package-lock.json | 34 ++++-------------------- packages/compass-collection/package.json | 4 +-- 2 files changed, 7 insertions(+), 31 deletions(-) diff --git a/package-lock.json b/package-lock.json index bff575baed1..11592904631 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48163,7 +48163,7 @@ "@mongodb-js/compass-app-stores": "^7.64.1", "@mongodb-js/compass-components": "^1.54.1", "@mongodb-js/compass-connections": "^1.78.1", - "@mongodb-js/compass-editor": "^0.55.0", + "@mongodb-js/compass-editor": "^0.56.1", "@mongodb-js/compass-generative-ai": "^0.57.1", "@mongodb-js/compass-logging": "^1.7.19", "@mongodb-js/compass-telemetry": "^1.16.1", @@ -48171,7 +48171,7 @@ "@mongodb-js/compass-workspaces": "^0.59.1", "@mongodb-js/connection-info": "^0.21.1", "@mongodb-js/mongodb-constants": "^0.14.0", - "bson": "^6.10.1", + "bson": "^6.10.4", "compass-preferences-model": "^2.57.1", "hadron-document": "^8.10.4", "mongodb": "^6.19.0", @@ -48207,29 +48207,6 @@ "xvfb-maybe": "^0.2.1" } }, - "packages/compass-collection/node_modules/@mongodb-js/compass-editor": { - "version": "0.55.0", - "resolved": "https://registry.npmjs.org/@mongodb-js/compass-editor/-/compass-editor-0.55.0.tgz", - "integrity": "sha512-+4B1NtWFBCaKkyCWg0nlbEYrVTOc/SjwxJNtiXjMGy6CPnsCSoHsxO0aoNvZrjR8Px9PL0f2rvnplNHBL7yoxw==", - "license": "SSPL", - "dependencies": { - "@codemirror/autocomplete": "^6.18.6", - "@codemirror/commands": "^6.8.1", - "@codemirror/lang-javascript": "^6.2.4", - "@codemirror/lang-json": "^6.0.2", - "@codemirror/language": "^6.11.2", - "@codemirror/lint": "^6.8.5", - "@codemirror/state": "^6.5.2", - "@codemirror/view": "^6.38.0", - "@lezer/highlight": "^1.2.1", - "@mongodb-js/compass-components": "^1.53.0", - "@mongodb-js/mongodb-constants": "^0.14.0", - "mongodb-query-parser": "^4.3.0", - "polished": "^4.2.2", - "prettier": "^2.7.1", - "react": "^17.0.2" - } - }, "packages/compass-collection/node_modules/@mongodb-js/mongodb-constants": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/@mongodb-js/mongodb-constants/-/mongodb-constants-0.14.0.tgz", @@ -61987,7 +61964,7 @@ "@mongodb-js/compass-app-stores": "^7.64.1", "@mongodb-js/compass-components": "^1.54.1", "@mongodb-js/compass-connections": "^1.78.1", - "@mongodb-js/compass-editor": "^0.55.0", + "@mongodb-js/compass-editor": "^0.56.1", "@mongodb-js/compass-generative-ai": "^0.57.1", "@mongodb-js/compass-logging": "^1.7.19", "@mongodb-js/compass-telemetry": "^1.16.1", @@ -62007,7 +61984,7 @@ "@types/react": "^17.0.5", "@types/react-dom": "^17.0.10", "@types/sinon-chai": "^3.2.5", - "bson": "^6.10.1", + "bson": "^6.10.4", "chai": "^4.3.6", "compass-preferences-model": "^2.57.1", "depcheck": "^1.4.1", @@ -62030,8 +62007,7 @@ }, "dependencies": { "@mongodb-js/compass-editor": { - "version": "0.55.0", - "resolved": "https://registry.npmjs.org/@mongodb-js/compass-editor/-/compass-editor-0.55.0.tgz", + "version": "https://registry.npmjs.org/@mongodb-js/compass-editor/-/compass-editor-0.55.0.tgz", "integrity": "sha512-+4B1NtWFBCaKkyCWg0nlbEYrVTOc/SjwxJNtiXjMGy6CPnsCSoHsxO0aoNvZrjR8Px9PL0f2rvnplNHBL7yoxw==", "requires": { "@codemirror/autocomplete": "^6.18.6", diff --git a/packages/compass-collection/package.json b/packages/compass-collection/package.json index 9cbb949b629..d0ee81c25e1 100644 --- a/packages/compass-collection/package.json +++ b/packages/compass-collection/package.json @@ -53,7 +53,7 @@ "@mongodb-js/compass-app-stores": "^7.64.1", "@mongodb-js/compass-components": "^1.54.1", "@mongodb-js/compass-connections": "^1.78.1", - "@mongodb-js/compass-editor": "^0.55.0", + "@mongodb-js/compass-editor": "^0.56.1", "@mongodb-js/compass-generative-ai": "^0.57.1", "@mongodb-js/compass-logging": "^1.7.19", "@mongodb-js/compass-telemetry": "^1.16.1", @@ -61,7 +61,7 @@ "@mongodb-js/compass-workspaces": "^0.59.1", "@mongodb-js/connection-info": "^0.21.1", "@mongodb-js/mongodb-constants": "^0.14.0", - "bson": "^6.10.1", + "bson": "^6.10.4", "compass-preferences-model": "^2.57.1", "hadron-document": "^8.10.4", "mongodb": "^6.19.0", From 70f266ef40eb33512b985e6335013db0ddaf4aab Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Mon, 29 Sep 2025 12:16:12 -0400 Subject: [PATCH 37/40] Merge conflict --- packages/connection-form/src/utils/csfle-handler.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/connection-form/src/utils/csfle-handler.spec.ts b/packages/connection-form/src/utils/csfle-handler.spec.ts index f1a83ab9c93..3ba94a0295b 100644 --- a/packages/connection-form/src/utils/csfle-handler.spec.ts +++ b/packages/connection-form/src/utils/csfle-handler.spec.ts @@ -468,7 +468,7 @@ describe('csfle-handler', function () { it('records the error for invalid shell BSON text', function () { expect(textToEncryptedFieldConfig('{')).to.deep.equal({ - '$compass.error': 'Unexpected token (3:0)', + '$compass.error': 'Unexpected token (3:0) in (\n{\n)', '$compass.rawText': '{', }); }); From 3903751e99ed0ae73bc9201e12c9b98ae727d18d Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Mon, 29 Sep 2025 12:37:41 -0400 Subject: [PATCH 38/40] Merge conflict --- packages/compass-crud/src/stores/crud-store.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/compass-crud/src/stores/crud-store.spec.ts b/packages/compass-crud/src/stores/crud-store.spec.ts index 034cd1b5a0d..d2766b88fa0 100644 --- a/packages/compass-crud/src/stores/crud-store.spec.ts +++ b/packages/compass-crud/src/stores/crud-store.spec.ts @@ -2591,7 +2591,7 @@ describe('store', function () { 'SyntaxError' ); expect(store.state.bulkUpdate.syntaxError?.message).to.equal( - 'Unexpected token (2:25)' + 'Unexpected token (2:25) in (\n{ $set: { anotherField: } }\n)' ); await store.updateBulkUpdatePreview('{ $set: { anotherField: 2 } }'); From f8cda08c4e7f6383a0912ac7a4f6e461c47b6157 Mon Sep 17 00:00:00 2001 From: Jacob Lu <43422771+jcobis@users.noreply.github.com> Date: Tue, 30 Sep 2025 10:58:32 -0400 Subject: [PATCH 39/40] Escape character Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .../mock-data-generator-modal/script-generation-utils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.ts b/packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.ts index 8e59655e297..597d27882c9 100644 --- a/packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.ts +++ b/packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.ts @@ -81,9 +81,9 @@ db.getCollection(${JSON.stringify( )}).insertMany(documents); console.log(\`Successfully inserted \${documents.length} documents into ${options.databaseName.replace( - /[`$]/g, // Escape backticks and dollar signs + /[\\`$]/g, // Escape backslashes, backticks and dollar signs '\\$&' - )}.${options.collectionName.replace(/[`$]/g, '\\$&')}\`);`; + )}.${options.collectionName.replace(/[\\`$]/g, '\\$&')}\`);`; // Format the script using prettier const script = prettify(unformattedScript, 'javascript'); From 18eeff0193b2663b1cf4213f9278bdfe8694ba40 Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Tue, 30 Sep 2025 11:08:06 -0400 Subject: [PATCH 40/40] Code comment --- .../mock-data-generator-modal/script-generation-utils.spec.ts | 4 ++-- .../mock-data-generator-modal/script-generation-utils.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.spec.ts b/packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.spec.ts index 361366fcec2..a18cefb4427 100644 --- a/packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.spec.ts +++ b/packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.spec.ts @@ -447,7 +447,7 @@ describe('Script Generation', () => { // Verify malicious code in name is safely contained in code comment expect(result.script).to.contain( - '// Generated for collection: test`; require("fs").rmSync("/"); //.my "collection"' + '// Generated for database: test`; require("fs").rmSync("/"); //; collection: my "collection"' ); // Test that the generated document code is executable @@ -475,7 +475,7 @@ describe('Script Generation', () => { if (result.success) { // Verify newlines are replaced with spaces in comments to prevent syntax errors expect(result.script).to.contain( - '// Generated for collection: test with newlines.coll with returns' + '// Generated for database: test with newlines; collection: coll with returns' ); // Verify the script is still syntactically valid diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.ts b/packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.ts index 597d27882c9..15687a4b76d 100644 --- a/packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.ts +++ b/packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.ts @@ -53,10 +53,10 @@ export function generateScript( // Generate unformatted script const unformattedScript = `// Mock Data Generator Script -// Generated for collection: ${options.databaseName.replace( +// Generated for database: ${options.databaseName.replace( /[\r\n]/g, // Prevent newlines in names that could break the comment ' ' - )}.${options.collectionName.replace(/[\r\n]/g, ' ')} + )}; collection: ${options.collectionName.replace(/[\r\n]/g, ' ')} // Document count: ${options.documentCount} const { faker } = require('@faker-js/faker');