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 f20bb70d2f5..4e7a7b2e991 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 @@ -31,6 +31,7 @@ import RawSchemaConfirmationScreen from './raw-schema-confirmation-screen'; import FakerSchemaEditorScreen from './faker-schema-editor-screen'; import ScriptScreen from './script-screen'; import DocumentCountScreen from './document-count-screen'; +import PreviewScreen from './preview-screen'; const footerStyles = css` flex-direction: row; @@ -95,7 +96,15 @@ const MockDataGeneratorModal = ({ /> ); case MockDataGeneratorStep.PREVIEW_DATA: - return <>; // TODO: CLOUDP-333857 + 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..33df025ed52 --- /dev/null +++ b/packages/compass-collection/src/components/mock-data-generator-modal/preview-screen.tsx @@ -0,0 +1,67 @@ +import React, { useMemo } from 'react'; +import { + css, + spacing, + Body, + DocumentList, +} from '@mongodb-js/compass-components'; +import HadronDocument from 'hadron-document'; +import type { FakerSchema } from './types'; +import { generateDocument } from './script-generation-utils'; + +const descriptionStyles = css({ + marginBottom: spacing[200], +}); + +const documentContainerStyles = css({ + display: 'flex', + flexDirection: 'column', + gap: spacing[300], +}); + +const documentWrapperStyles = css({ + border: '1px solid #E8EDEB', + borderRadius: '6px', + padding: spacing[200], +}); + +interface PreviewScreenProps { + confirmedFakerSchema: FakerSchema; +} + +const NUM_SAMPLE_DOCUMENTS = 3; + +function PreviewScreen({ confirmedFakerSchema }: PreviewScreenProps) { + const sampleDocuments = useMemo(() => { + const documents = []; + for (let i = 0; i < NUM_SAMPLE_DOCUMENTS; i++) { + const plainDoc = generateDocument(confirmedFakerSchema); + const hadronDoc = new HadronDocument(plainDoc); + hadronDoc.expand(); // Expand by default for better preview + documents.push(hadronDoc); + } + + return documents; + }, [confirmedFakerSchema]); + + return ( +
+ + Preview Mock Data + + + Below is a sample of documents that will be generated based on your + script + +
+ {sampleDocuments.map((doc, index) => ( +
+ +
+ ))} +
+
+ ); +} + +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 a18cefb4427..ef62a3cba18 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'; /** @@ -1421,4 +1421,151 @@ describe('Script Generation', () => { } }); }); + + describe('generateDocument', () => { + it('should generate document with simple flat fields', () => { + const schema = { + name: { + mongoType: 'String' as const, + fakerMethod: 'person.fullName', + fakerArgs: [], + probability: 1.0, + }, + age: { + mongoType: 'Number' as const, + fakerMethod: 'number.int', + fakerArgs: [{ json: '{"min": 18, "max": 65}' }], + 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.age).to.be.at.least(18).and.at.most(65); + }); + + it('should generate document with arrays', () => { + const schema = { + 'tags[]': { + mongoType: 'String' as const, + fakerMethod: 'lorem.word', + fakerArgs: [], + probability: 1.0, + }, + }; + + const document = generateDocument(schema, { 'tags[]': 2 }); + + expect(document).to.be.an('object'); + expect(document).to.have.property('tags'); + expect(document.tags).to.be.an('array').with.length(2); + for (const tag of document.tags as string[]) { + expect(tag).to.be.a('string').and.not.be.empty; + } + }); + + it('should generate document with complex nested arrays and custom lengths', () => { + const schema = { + 'users[].posts[].tags[]': { + mongoType: 'String' as const, + fakerMethod: 'lorem.word', + fakerArgs: [], + probability: 1.0, + }, + 'matrix[][]': { + mongoType: 'Number' as const, + fakerMethod: 'number.int', + fakerArgs: [{ json: '{"min": 1, "max": 10}' }], + probability: 1.0, + }, + }; + + const arrayLengthMap = { + 'users[]': 2, + 'users[].posts[]': 3, + 'users[].posts[].tags[]': 4, + 'matrix[]': 2, + 'matrix[][]': 3, + }; + + const document = generateDocument(schema, arrayLengthMap); + + expect(document).to.be.an('object'); + + // Check users array structure + expect(document).to.have.property('users'); + expect(document.users).to.be.an('array').with.length(2); + + // Check nested structure with proper types + const users = document.users as Array<{ + posts: Array<{ tags: string[] }>; + }>; + + for (const user of users) { + expect(user).to.be.an('object'); + expect(user).to.have.property('posts'); + expect(user.posts).to.be.an('array').with.length(3); + + for (const post of user.posts) { + expect(post).to.be.an('object'); + expect(post).to.have.property('tags'); + expect(post.tags).to.be.an('array').with.length(4); + + for (const tag of post.tags) { + expect(tag).to.be.a('string').and.not.be.empty; + } + } + } + + // Check matrix (2D array) + expect(document).to.have.property('matrix'); + expect(document.matrix).to.be.an('array').with.length(2); + + const matrix = document.matrix as number[][]; + for (const row of matrix) { + expect(row).to.be.an('array').with.length(3); + for (const cell of row) { + expect(cell).to.be.a('number').and.be.at.least(1).and.at.most(10); + } + } + }); + + it('should handle probability fields correctly', () => { + const schema = { + name: { + mongoType: 'String' as const, + fakerMethod: 'person.fullName', + fakerArgs: [], + probability: 1.0, + }, + optionalField: { + mongoType: 'String' as const, + fakerMethod: 'lorem.word', + fakerArgs: [], + probability: 0.0, // Should never appear + }, + alwaysPresent: { + mongoType: 'Number' as const, + fakerMethod: 'number.int', + 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('alwaysPresent'); + expect(document.alwaysPresent).to.be.a('number'); + // optionalField should not be present due to 0.0 probability + expect(document).to.not.have.property('optionalField'); + }); + }); }); 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 3138663fd55..7b1f5d74e35 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,6 +1,7 @@ import type { MongoDBFieldType } from '@mongodb-js/compass-generative-ai'; import type { FakerFieldMapping } from './types'; import { prettify } from '@mongodb-js/compass-editor'; +import { faker } from '@faker-js/faker/locale/en'; export type FakerArg = | string @@ -568,3 +569,170 @@ export function formatFakerArgs(fakerArgs: FakerArg[]): string { return stringifiedArgs.join(', '); } + +type Document = Record; + +/** + * Generates documents for the PreviewScreen component. + * Executes faker methods to create actual document objects. + */ +export function generateDocument( + fakerSchema: Record, + arrayLengthMap: ArrayLengthMap = {} +): Document { + const structure = buildDocumentStructure(fakerSchema); + return constructDocumentValues(structure, arrayLengthMap); +} + +function computeValue( + elementType: ArrayStructure | FakerFieldMapping | DocumentStructure, + arrayLengthMap: ArrayLengthMap, + currentPath: string +) { + try { + if ('mongoType' in elementType) { + // It's a field mapping + const mapping = elementType as FakerFieldMapping; + + // Default to 1.0 for invalid probability values + let probability = 1.0; + if ( + typeof mapping.probability === 'number' && + mapping.probability >= 0 && + mapping.probability <= 1 + ) { + probability = mapping.probability; + } + + const shouldIncludeField = + probability >= 1.0 || Math.random() < probability; + if (shouldIncludeField) { + return generateFakerValue(mapping); + } + } else if ('type' in elementType && elementType.type === 'array') { + return constructArrayValues( + elementType as ArrayStructure, + arrayLengthMap, + `${currentPath}[]` + ); + } else { + return constructDocumentValues( + elementType as DocumentStructure, + arrayLengthMap, + currentPath + ); + } + } catch { + // Skip invalid faker methods + } +} + +/** + * Construct actual document values from document structure. + * Mirrors renderDocumentCode but executes faker calls instead of generating code. + */ +function constructDocumentValues( + structure: DocumentStructure, + arrayLengthMap: ArrayLengthMap = {}, + currentPath: string = '' +) { + const result: Document = {}; + for (const [fieldName, value] of Object.entries(structure)) { + const newPath = currentPath ? `${currentPath}.${fieldName}` : fieldName; + const val = computeValue(value, arrayLengthMap, newPath); + if (val !== undefined) { + result[fieldName] = val; + } + } + return result; +} + +/** + * Construct array values from array structure. + * Mirrors renderArrayCode but executes faker calls instead of generating code. + */ +function constructArrayValues( + arrayStructure: ArrayStructure, + arrayLengthMap: ArrayLengthMap, + currentPath: string +) { + const elementType = arrayStructure.elementType; + + // Get array length for this dimension + let arrayLength = DEFAULT_ARRAY_LENGTH; + if (arrayLengthMap[currentPath] !== undefined) { + arrayLength = arrayLengthMap[currentPath]; + } + const result: unknown[] = []; + for (let i = 0; i < arrayLength; i++) { + result.push(computeValue(elementType, arrayLengthMap, currentPath)); + } + return result; +} + +/** + * Prepare faker arguments for execution. + * Converts FakerArg[] to actual values that can be passed to faker methods. + */ +function prepareFakerArgs(fakerArgs: FakerArg[]): unknown[] { + const preparedArgs: unknown[] = []; + + for (const arg of fakerArgs) { + if ( + typeof arg === 'string' || + typeof arg === 'number' || + typeof arg === 'boolean' + ) { + preparedArgs.push(arg); + } else if (typeof arg === 'object' && arg !== null && 'json' in arg) { + // Parse JSON objects + try { + const jsonArg = arg as { json: string }; + preparedArgs.push(JSON.parse(jsonArg.json)); + } catch { + // Skip invalid JSON + continue; + } + } + } + + return preparedArgs; +} + +/** + * Execute faker method to generate actual values. + * Mirrors generateFakerCall but executes the call instead of generating code. + */ +function generateFakerValue( + mapping: FakerFieldMapping +): string | number | boolean | Date | null | undefined { + const method = + mapping.fakerMethod === 'unrecognized' + ? getDefaultFakerMethod(mapping.mongoType) + : mapping.fakerMethod; + + try { + // Navigate to the faker method + const methodParts = method.split('.'); + let fakerMethod: unknown = faker; + for (const part of methodParts) { + fakerMethod = (fakerMethod as Record)[part]; + if (!fakerMethod) { + throw new Error(`Faker method not found: ${method}`); + } + } + + // Prepare arguments + const args = prepareFakerArgs(mapping.fakerArgs); + + // Call the faker method + const result = (fakerMethod as (...args: unknown[]) => unknown).apply( + faker, + args + ); + + return result as string | number | boolean | Date | null | undefined; + } catch { + return undefined; + } +}