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..a02857ed1bc 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 @@ -25,6 +25,7 @@ import { import RawSchemaConfirmationScreen from './raw-schema-confirmation-screen'; import FakerSchemaEditorScreen from './faker-schema-editor-screen'; import ScriptScreen from './script-screen'; +import PreviewScreen from './preview-screen'; const footerStyles = css` flex-direction: row; @@ -79,9 +80,22 @@ const MockDataGeneratorModal = ({ /> ); case MockDataGeneratorStep.DOCUMENT_COUNT: - return <>; // TODO: CLOUDP-333856 + return <>; // TODO(CLOUDP-333856) case MockDataGeneratorStep.PREVIEW_DATA: - return <>; // TODO: CLOUDP-333857 + // TODO(CLOUDP-333855): Apply results from schema editor confirmation + // + // function validateFakerSchema(input: FakerSchemaMapping): asserts input is ValidatedFakerSchemaMapping { + // ... + // } + return ( + + ); case MockDataGeneratorStep.GENERATE_DATA: return ; } diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/preview-screen.tsx b/packages/compass-collection/src/components/mock-data-generator-modal/preview-screen.tsx new file mode 100644 index 00000000000..1eb5b2a631f --- /dev/null +++ b/packages/compass-collection/src/components/mock-data-generator-modal/preview-screen.tsx @@ -0,0 +1,42 @@ +import React, { useMemo } from 'react'; +import { css, spacing, Body, Code } from '@mongodb-js/compass-components'; +import type { FakerSchema } from './types'; +import { generateDocument } from './script-generation-utils'; + +const descriptionStyles = css({ + marginBottom: spacing[200], +}); + +interface PreviewScreenProps { + confirmedFakerSchema: FakerSchema; +} + +const NUM_SAMPLE_DOCUMENTS = 5; + +function PreviewScreen({ confirmedFakerSchema }: PreviewScreenProps) { + const sampleDocuments = useMemo(() => { + const documents = []; + for (let i = 0; i < NUM_SAMPLE_DOCUMENTS; i++) { + documents.push(generateDocument(confirmedFakerSchema)); + } + + return documents; + }, [confirmedFakerSchema]); + + return ( +
+ + Preview Mock Data + + + Below is a sample of documents that will be generated based on your + script + + + {JSON.stringify(sampleDocuments, null, 2)} + +
+ ); +} + +export default PreviewScreen; 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..705e7f873cb 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,6 +1,6 @@ import { expect } from 'chai'; import { faker } from '@faker-js/faker/locale/en'; -import { generateScript } from './script-generation-utils'; +import { generateScript, generateDocument } from './script-generation-utils'; import type { FakerFieldMapping } from './types'; /** @@ -1253,4 +1253,167 @@ describe('Script Generation', () => { } }); }); + + describe('generateDocument', () => { + it('should generate document with simple flat fields of mixed types', () => { + const schema = { + name: { + mongoType: 'String' as const, + fakerMethod: 'person.fullName', + fakerArgs: [], + probability: 1.0, + }, + age: { + mongoType: 'Number' as const, + fakerMethod: 'number.int', + fakerArgs: [], + probability: 1.0, + }, + isActive: { + mongoType: 'Boolean' as const, + fakerMethod: 'datatype.boolean', + fakerArgs: [], + probability: 1.0, + }, + createdAt: { + mongoType: 'Date' as const, + fakerMethod: 'date.recent', + fakerArgs: [], + probability: 1.0, + }, + _id: { + mongoType: 'ObjectId' as const, + fakerMethod: 'database.mongodbObjectId', + fakerArgs: [], + probability: 1.0, + }, + }; + + const document = generateDocument(schema); + + expect(document).to.be.an('object'); + expect(document).to.have.property('name'); + expect(document.name).to.be.a('string').and.not.be.empty; + expect(document).to.have.property('age'); + expect(document.age).to.be.a('number'); + expect(document).to.have.property('isActive'); + expect(document.isActive).to.be.a('boolean'); + expect(document).to.have.property('createdAt'); + expect(document.createdAt).to.be.a('date'); + expect(document).to.have.property('_id'); + expect(document._id).to.be.a('string'); + }); + + it('should generate document with multi-dimensional arrays of mixed types', () => { + const schema = { + 'numberMatrix[][]': { + mongoType: 'Number' as const, + fakerMethod: 'number.int', + fakerArgs: [], + probability: 1.0, + }, + 'stringMatrix[][]': { + mongoType: 'String' as const, + fakerMethod: 'lorem.word', + fakerArgs: [], + probability: 1.0, + }, + 'booleanGrid[][]': { + mongoType: 'Boolean' as const, + fakerMethod: 'datatype.boolean', + fakerArgs: [], + probability: 1.0, + }, + }; + + const arrayLengthMap = { + numberMatrix: [2, 3], + stringMatrix: [2, 2], + booleanGrid: [3, 2], + }; + + const document = generateDocument(schema, arrayLengthMap); + + expect(document).to.be.an('object'); + expect(document).to.have.property('numberMatrix'); + expect(document.numberMatrix).to.be.an('array').with.length(2); + expect(document.numberMatrix[0]).to.be.an('array').with.length(3); + expect(document.numberMatrix[0][0]).to.be.a('number'); + + expect(document).to.have.property('stringMatrix'); + expect(document.stringMatrix).to.be.an('array').with.length(2); + expect(document.stringMatrix[0]).to.be.an('array').with.length(2); + expect(document.stringMatrix[0][0]).to.be.a('string').and.not.be.empty; + + expect(document).to.have.property('booleanGrid'); + expect(document.booleanGrid).to.be.an('array').with.length(3); + expect(document.booleanGrid[0]).to.be.an('array').with.length(2); + expect(document.booleanGrid[0][0]).to.be.a('boolean'); + }); + + it('should handle complex nested structures with arrays and objects', () => { + const schema = { + 'company.name': { + mongoType: 'String' as const, + fakerMethod: 'company.name', + fakerArgs: [], + probability: 1.0, + }, + 'company.employees[].name': { + mongoType: 'String' as const, + fakerMethod: 'person.fullName', + fakerArgs: [], + probability: 1.0, + }, + 'company.employees[].email': { + mongoType: 'String' as const, + fakerMethod: 'internet.email', + fakerArgs: [], + probability: 1.0, + }, + 'company.employees[].skills[]': { + mongoType: 'String' as const, + fakerMethod: 'lorem.word', + fakerArgs: [], + probability: 1.0, + }, + 'company.founded': { + mongoType: 'Date' as const, + fakerMethod: 'date.past', + fakerArgs: [], + probability: 1.0, + }, + 'company.isActive': { + mongoType: 'Boolean' as const, + fakerMethod: 'datatype.boolean', + fakerArgs: [], + probability: 1.0, + }, + }; + + const document = generateDocument(schema); + + expect(document).to.be.an('object'); + expect(document).to.have.property('company'); + expect(document.company).to.be.an('object'); + expect(document.company).to.have.property('name'); + expect(document.company.name).to.be.a('string').and.not.be.empty; + expect(document.company).to.have.property('founded'); + expect(document.company.founded).to.be.a('date'); + expect(document.company).to.have.property('isActive'); + expect(document.company.isActive).to.be.a('boolean'); + expect(document.company).to.have.property('employees'); + expect(document.company.employees).to.be.an('array').with.length(3); + + const firstEmployee = document.company.employees[0]; + expect(firstEmployee).to.be.an('object'); + expect(firstEmployee).to.have.property('name'); + expect(firstEmployee.name).to.be.a('string').and.not.be.empty; + expect(firstEmployee).to.have.property('email'); + expect(firstEmployee.email).to.be.a('string').and.include('@'); + expect(firstEmployee).to.have.property('skills'); + expect(firstEmployee.skills).to.be.an('array').with.length(3); + expect(firstEmployee.skills[0]).to.be.a('string').and.not.be.empty; + }); + }); }); 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..ef4c3a6ac2f 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,11 +1,21 @@ +import type { Document } from 'mongodb'; import type { MongoDBFieldType } from '@mongodb-js/compass-generative-ai'; -import type { FakerFieldMapping } from './types'; +import type { FakerFieldMapping, FakerArg } from './types'; +import type { SampleValue } from '../../schema-analysis-types'; -export type FakerArg = string | number | boolean | { json: string }; +import { faker } from '@faker-js/faker/locale/en'; const DEFAULT_ARRAY_LENGTH = 3; const INDENT_SIZE = 2; +/** + * Type representing the possible values that can be stored in a generated array. This includes: + * - Primitive values + * - MongoDB documents (objects) + * - Nested arrays (for multi-dimensional arrays) + */ +type ArrayElementValue = SampleValue | Document | ArrayElementValue[]; + // Array length configuration for different array types export type ArrayLengthMap = { [fieldName: string]: @@ -585,3 +595,138 @@ export function formatFakerArgs(fakerArgs: FakerArg[]): string { return stringifiedArgs.join(', '); } + +/** + * Generates documents for the PreviewScreen component. + * Now works directly with the optimized object format. + */ +export function generateDocument( + fakerSchema: Record, + arrayLengthMap: ArrayLengthMap = {} +): Document { + const structure = buildDocumentStructure(fakerSchema); + return constructDocumentValues(structure, arrayLengthMap); +} + +function generateFakerValue( + mapping: FakerFieldMapping +): string | number | boolean | null | undefined { + const method = + mapping.fakerMethod === 'unrecognized' + ? getDefaultFakerMethod(mapping.mongoType) + : mapping.fakerMethod; + + try { + // e.g., "person.firstName" -> ["person", "firstName"]) + const [moduleName, methodName] = method.split('.'); + + // This check should not fail if fakerSchema is validated properly + if (typeof (faker as any)[moduleName]?.[methodName] !== 'function') { + return null; + } + + const processedArgs = mapping.fakerArgs.map((arg) => { + if (typeof arg === 'object' && arg !== null && 'json' in arg) { + return JSON.parse(arg.json); + } + return arg; + }); + + // Call the faker method with processed arguments + const fakerModule = (faker as any)[moduleName]; + return fakerModule[methodName](...processedArgs); + } catch { + return null; + } +} + +function constructDocumentValues( + structure: DocumentStructure, + arrayLengthMap: ArrayLengthMap = {} +): Document { + const result: Document = {}; + + for (const [fieldName, value] of Object.entries(structure)) { + if ('mongoType' in value) { + // It's a field mapping - generate the actual value + const mapping = value as FakerFieldMapping; + result[fieldName] = generateFakerValue(mapping); + } else if ('type' in value && value.type === 'array') { + // It's an array - generate array of values + const arrayStructure = value as ArrayStructure; + result[fieldName] = constructArrayValues( + arrayStructure, + fieldName, + arrayLengthMap, + 0 // Start at dimension 0 + ); + } else { + // It's a nested object - recursively generate + const arrayInfo = arrayLengthMap[fieldName]; + const nestedArrayLengthMap = + arrayInfo && !Array.isArray(arrayInfo) && 'elements' in arrayInfo + ? arrayInfo.elements + : {}; + + result[fieldName] = constructDocumentValues( + value as DocumentStructure, + nestedArrayLengthMap + ); + } + } + + return result; +} + +function constructArrayValues( + arrayStructure: ArrayStructure, + fieldName: string = '', + arrayLengthMap: ArrayLengthMap = {}, + dimensionIndex: number = 0 +): ArrayElementValue[] { + const elementType = arrayStructure.elementType; + + // Get array length for this dimension (same logic as renderArrayCode) + 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; + } else if (arrayInfo && 'length' in arrayInfo) { + // Array of objects/documents + arrayLength = arrayInfo.length ?? DEFAULT_ARRAY_LENGTH; + } + + const result: ArrayElementValue[] = []; + for (let i = 0; i < arrayLength; i++) { + if ('mongoType' in elementType) { + // Array of primitives + result.push(generateFakerValue(elementType as FakerFieldMapping)); + } else if ('type' in elementType && elementType.type === 'array') { + // Nested array (e.g., matrix[][]) - keep same fieldName, increment dimension + result.push( + constructArrayValues( + elementType as ArrayStructure, + fieldName, + arrayLengthMap, + dimensionIndex + 1 // Next dimension + ) + ); + } else { + // Array of objects + const nestedArrayLengthMap = + arrayInfo && !Array.isArray(arrayInfo) && 'elements' in arrayInfo + ? arrayInfo.elements + : {}; + result.push( + constructDocumentValues( + elementType as DocumentStructure, + nestedArrayLengthMap + ) + ); + } + } + + return result; +} 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 af5150fbd55..8f8add2a3f0 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,6 +1,7 @@ import type { MockDataSchemaResponse } from '@mongodb-js/compass-generative-ai'; import type { MongoDBFieldType } from '@mongodb-js/compass-generative-ai'; -import type { FakerArg } from './script-generation-utils'; + +export type FakerArg = string | number | boolean | { json: string }; export enum MockDataGeneratorStep { SCHEMA_CONFIRMATION = 'SCHEMA_CONFIRMATION', @@ -37,8 +38,10 @@ export type MockDataGeneratorState = | MockDataGeneratorCompletedState | MockDataGeneratorErrorState; +// LLM output format (array with fieldPath as property) export type LlmFakerMapping = MockDataSchemaResponse['fields'][number]; +// Processed format (object value without fieldPath) export interface FakerFieldMapping { mongoType: MongoDBFieldType; fakerMethod: string; @@ -46,4 +49,14 @@ export interface FakerFieldMapping { probability?: number; // 0.0 - 1.0 frequency of field (defaults to 1.0) } +// Optimized object format (fieldPath as key, FakerFieldMapping as value) export type FakerSchema = Record; + +/** + * The faker schema is validated if it has been (1) confirmed by the user and + * (2) TODO(CLOUDP-333855): pre-processed to prevent harmful calls like those that + * block the main thread or cause out of memory errors + */ +export type ValidatedFakerSchema = FakerSchema & { + readonly __brand: unique symbol; +};