From f4ce18bdb8644aca503847cafe779ead901166f5 Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Tue, 9 Sep 2025 17:56:35 -0400 Subject: [PATCH 01/31] WIP --- .../script-generation-utils.spec.ts | 235 +++++++++++++ .../script-generation-utils.ts | 316 ++++++++++++++++++ 2 files changed, 551 insertions(+) create mode 100644 packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.spec.ts create mode 100644 packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.ts 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 new file mode 100644 index 00000000000..b7b4116fc1b --- /dev/null +++ b/packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.spec.ts @@ -0,0 +1,235 @@ +import { expect } from 'chai'; +import { generateScript, type FieldMapping } from './script-generation-utils'; + +describe('Simple Script Generation', () => { + const createFieldMapping = (fakerMethod: string): FieldMapping => ({ + mongoType: 'String', + fakerMethod, + fakerArgs: [], + }); + + describe('Basic field generation', () => { + it('should generate script for simple fields', () => { + const schema = { + name: createFieldMapping('person.fullName'), + email: createFieldMapping('internet.email'), + }; + + const result = generateScript(schema, { + databaseName: 'testdb', + collectionName: 'users', + documentCount: 5, + }); + + expect(result.success).to.equal(true); + if (result.success) { + expect(result.script).to.contain('use("testdb")'); + expect(result.script).to.contain('faker.person.fullName()'); + expect(result.script).to.contain('faker.internet.email()'); + expect(result.script).to.contain('insertMany'); + } + }); + }); + + describe('Array generation', () => { + it('should generate script for simple arrays', () => { + const schema = { + 'tags[]': createFieldMapping('lorem.word'), + }; + + const result = generateScript(schema, { + databaseName: 'testdb', + collectionName: 'posts', + documentCount: 3, + }); + + expect(result.success).to.equal(true); + if (result.success) { + expect(result.script).to.contain('Array.from'); + expect(result.script).to.contain('faker.lorem.word()'); + } + }); + + it('should generate script for arrays of objects', () => { + const schema = { + 'users[].name': createFieldMapping('person.fullName'), + 'users[].email': createFieldMapping('internet.email'), + }; + + const result = generateScript(schema, { + databaseName: 'testdb', + collectionName: 'teams', + documentCount: 2, + }); + + expect(result.success).to.equal(true); + if (result.success) { + expect(result.script).to.contain('Array.from'); + expect(result.script).to.contain('faker.person.fullName()'); + expect(result.script).to.contain('faker.internet.email()'); + // Should have nested object structure + expect(result.script).to.match(/name:\s*faker\.person\.fullName\(\)/); + expect(result.script).to.match(/email:\s*faker\.internet\.email\(\)/); + } + }); + + it('should generate script for multi-dimensional arrays', () => { + const schema = { + 'matrix[][]': createFieldMapping('number.int'), + }; + + const result = generateScript(schema, { + databaseName: 'testdb', + collectionName: 'data', + documentCount: 1, + }); + + expect(result.success).to.equal(true); + if (result.success) { + // Should have nested Array.from calls + expect(result.script).to.contain('Array.from'); + expect(result.script).to.contain('faker.number.int()'); + // Should have two levels of Array.from for 2D array + const arrayFromMatches = result.script.match(/Array\.from/g); + expect(arrayFromMatches?.length).to.be.greaterThanOrEqual(2); + } + }); + + it('should generate script for arrays of objects with arrays', () => { + const schema = { + 'users[].name': createFieldMapping('person.fullName'), + 'users[].tags[]': createFieldMapping('lorem.word'), + }; + + const result = generateScript(schema, { + databaseName: 'testdb', + collectionName: 'profiles', + documentCount: 1, + }); + + expect(result.success).to.equal(true); + if (result.success) { + expect(result.script).to.contain('faker.person.fullName()'); + expect(result.script).to.contain('faker.lorem.word()'); + // Should have nested structure: users array containing objects with tags arrays + expect(result.script).to.match(/name:\s*faker\.person\.fullName\(\)/); + expect(result.script).to.match(/tags:\s*Array\.from/); + } + }); + }); + + describe('Complex nested structures', () => { + it('should handle deeply nested object paths', () => { + const schema = { + 'users[].profile.address.street': createFieldMapping( + 'location.streetAddress' + ), + 'users[].profile.address.city': createFieldMapping('location.city'), + }; + + const result = generateScript(schema, { + databaseName: 'testdb', + collectionName: 'accounts', + documentCount: 1, + }); + + expect(result.success).to.equal(true); + if (result.success) { + expect(result.script).to.contain('faker.location.streetAddress()'); + expect(result.script).to.contain('faker.location.city()'); + // Should have nested object structure + expect(result.script).to.contain('profile'); + expect(result.script).to.contain('address'); + } + }); + + it('should handle mixed field types', () => { + const schema = { + title: createFieldMapping('lorem.sentence'), + 'authors[].name': createFieldMapping('person.fullName'), + 'authors[].books[]': createFieldMapping('lorem.words'), + publishedYear: createFieldMapping('date.recent'), + }; + + const result = generateScript(schema, { + databaseName: 'library', + collectionName: 'publications', + documentCount: 2, + }); + + expect(result.success).to.equal(true); + if (result.success) { + expect(result.script).to.contain('faker.lorem.sentence()'); + expect(result.script).to.contain('faker.person.fullName()'); + expect(result.script).to.contain('faker.lorem.words()'); + expect(result.script).to.contain('faker.date.recent()'); + } + }); + }); + + describe('Edge cases', () => { + it('should handle empty schema', () => { + const result = generateScript( + {}, + { + databaseName: 'testdb', + collectionName: 'empty', + documentCount: 1, + } + ); + + expect(result.success).to.equal(true); + if (result.success) { + expect(result.script).to.contain('use("testdb")'); + expect(result.script).to.contain('insertMany'); + // Should generate empty objects + expect(result.script).to.contain('{}'); + } + }); + + it('should handle single field', () => { + const schema = { + value: createFieldMapping('number.int'), + }; + + const result = generateScript(schema, { + databaseName: 'testdb', + collectionName: 'simple', + documentCount: 1, + }); + + expect(result.success).to.equal(true); + if (result.success) { + expect(result.script).to.contain('faker.number.int()'); + expect(result.script).to.match(/value:\s*faker\.number\.int\(\)/); + } + }); + + it('should handle multiple fields in the same nested object', () => { + const schema = { + 'profile.name': createFieldMapping('person.fullName'), + 'profile.email': createFieldMapping('internet.email'), + 'profile.age': createFieldMapping('number.int'), + }; + + const result = generateScript(schema, { + databaseName: 'testdb', + collectionName: 'users', + documentCount: 1, + }); + + expect(result.success).to.equal(true); + if (result.success) { + // All three fields should be present in the same profile object + expect(result.script).to.contain('faker.person.fullName()'); + expect(result.script).to.contain('faker.internet.email()'); + expect(result.script).to.contain('faker.number.int()'); + expect(result.script).to.contain('profile'); + // Should have all fields in nested structure + expect(result.script).to.match(/name:\s*faker\.person\.fullName\(\)/); + expect(result.script).to.match(/email:\s*faker\.internet\.email\(\)/); + expect(result.script).to.match(/age:\s*faker\.number\.int\(\)/); + } + }); + }); +}); 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 new file mode 100644 index 00000000000..7f81983397c --- /dev/null +++ b/packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.ts @@ -0,0 +1,316 @@ +export interface FieldMapping { + mongoType: string; + fakerMethod: string; + fakerArgs: any[]; // TODO: type this properly later +} + +export interface ScriptOptions { + documentCount: number; + databaseName: string; + collectionName: string; + // TODO: array lengths - for now use fixed length +} + +export interface ScriptResult { + script: string; + success: boolean; + error?: string; +} + +type DocumentStructure = { + [fieldName: string]: + | FieldMapping // Leaf: actual data field + | DocumentStructure // Object: nested fields + | ArrayStructure; // Array: repeated elements +}; + +interface ArrayStructure { + type: 'array'; + elementType: FieldMapping | DocumentStructure | ArrayStructure; +} + +/** + * Parse a field path into simple parts + * + * Examples: + * "name" → ["name"] + * "user.email" → ["user", "email"] + * "tags[]" → ["tags", "[]"] + * "users[].name" → ["users", "[]", "name"] + * "matrix[][]" → ["matrix", "[]", "[]"] + */ +function parseFieldPath(fieldPath: string): string[] { + const parts: string[] = []; + let current = ''; + + for (let i = 0; i < fieldPath.length; i++) { + const char = fieldPath[i]; + + if (char === '.') { + if (current) { + parts.push(current); + current = ''; + } + } else if (char === '[' && fieldPath[i + 1] === ']') { + if (current) { + parts.push(current); + current = ''; + } + parts.push('[]'); + i++; // Skip the ] + } else { + current += char; + } + } + + if (current) { + parts.push(current); + } + + return parts; +} +/** + * Build the document structure from all field paths + */ +function buildDocumentStructure( + schema: Record +): DocumentStructure { + const result: DocumentStructure = {}; + + // Process each field path + for (const [fieldPath, mapping] of Object.entries(schema)) { + const pathParts = parseFieldPath(fieldPath); + insertIntoStructure(result, pathParts, mapping); + } + + return result; +} + +/** + * Insert a field mapping into the structure at the given path + */ +function insertIntoStructure( + structure: DocumentStructure, + pathParts: string[], + mapping: FieldMapping +): void { + if (pathParts.length === 0) { + // This shouldn't happen + // TODO: log error + return; + } + + // Base case: insert root-level field mapping + if (pathParts.length === 1) { + const part = pathParts[0]; + if (part === '[]') { + // This shouldn't happen - array without field name + // TODO: log error + return; + } + structure[part] = mapping; + return; + } + + // Recursive case + const [firstPart, secondPart, ...remainingParts] = pathParts; + + if (secondPart === '[]') { + // This is an array field + // Initialize array structure if it doesn't exist yet + if ( + !structure[firstPart] || + typeof structure[firstPart] !== 'object' || + !('type' in structure[firstPart]) || + structure[firstPart].type !== 'array' + ) { + structure[firstPart] = { + type: 'array', + elementType: {}, + }; + } + + const arrayStructure = structure[firstPart] as ArrayStructure; + + if (remainingParts.length === 0) { + // Terminal case: Array of primitives (e.g., "tags[]") + // Directly assign the field mapping as the element type + arrayStructure.elementType = mapping; + } else if (remainingParts[0] === '[]') { + // Nested array case: Multi-dimensional arrays (e.g., "matrix[][]") + // Build nested array structure + let currentArray = arrayStructure; + let i = 0; + + // Process consecutive [] markers to build nested array structure + // Each iteration creates one array dimension (eg. matrix[][] = 2 iterations) + while (i < remainingParts.length && remainingParts[i] === '[]') { + // Create the next array dimension + currentArray.elementType = { + type: 'array', + elementType: {}, + }; + + // Move to the next nesting level + currentArray = currentArray.elementType; + i++; + } + + if (i < remainingParts.length) { + // This is an multi-dimensional array of documents (e.g., "matrix[][].name") + // Ensure we have a document structure for the remaining fields + if ( + typeof currentArray.elementType !== 'object' || + 'mongoType' in currentArray.elementType || + 'type' in currentArray.elementType + ) { + currentArray.elementType = {}; + } + // Recursively build the document + insertIntoStructure( + currentArray.elementType, + remainingParts.slice(i), + mapping + ); + } else { + // Pure multi-dimensional array - assign the mapping + currentArray.elementType = mapping; + } + } else { + // Object case: Array of documents with fields (e.g., "users[].name", "users[].profile.email") + // Only initialize if elementType isn't already a proper object structure + if ( + typeof arrayStructure.elementType !== 'object' || + 'mongoType' in arrayStructure.elementType || + 'type' in arrayStructure.elementType + ) { + arrayStructure.elementType = {}; + } + + // Recursively build the object structure for array elements + insertIntoStructure(arrayStructure.elementType, remainingParts, mapping); + } + } else { + // This is a regular object field + // Only initialize if it doesn't exist or isn't a plain object + if ( + !structure[firstPart] || + typeof structure[firstPart] !== 'object' || + 'type' in structure[firstPart] || + 'mongoType' in structure[firstPart] + ) { + structure[firstPart] = {}; + } + + insertIntoStructure( + structure[firstPart], + [secondPart, ...remainingParts], + mapping + ); + } +} + +/** + * Generate the final script + */ +export function generateScript( + schema: Record, + options: ScriptOptions +): ScriptResult { + try { + const structure = buildDocumentStructure(schema); + + const documentCode = generateDocumentCode(structure); + + const script = ` +// Generated Mock Data Script +const { faker } = require('@faker-js/faker'); + +function generateDocument() { + return ${documentCode}; +} + +// Generate documents +const documents = []; +for (let i = 0; i < ${options.documentCount}; i++) { + documents.push(generateDocument()); +} + +// TODO: Add database connection and insertion +console.log('Generated', documents.length, 'documents'); +`; + + return { + script, + success: true, + }; + } catch (error) { + return { + script: '', + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + }; + } +} + +/** + * Generate JavaScript object code from document structure + */ +function generateDocumentCode(structure: DocumentStructure): string { + // For each field in structure: + // - If FieldMapping: generate faker call + // - If DocumentStructure: generate nested object + // - If ArrayStructure: generate array + + const parts: string[] = []; + + for (const [fieldName, value] of Object.entries(structure)) { + if ('mongoType' in value) { + // It's a field mapping + const fakerCall = generateFakerCall(value as FieldMapping); + parts.push(` ${fieldName}: ${fakerCall}`); + } else if ('type' in value && value.type === 'array') { + // It's an array + const arrayCode = generateArrayCode(value as ArrayStructure); + parts.push(` ${fieldName}: ${arrayCode}`); + } else { + // It's a nested object: recursive call + const nestedCode = generateDocumentCode(value as DocumentStructure); + parts.push(` ${fieldName}: ${nestedCode}`); + } + } + + return `{\n${parts.join(',\n')}\n}`; +} + +/** + * Generate array code + */ +function generateArrayCode(arrayStructure: ArrayStructure): string { + const elementType = arrayStructure.elementType; + + // Fixed length for now - TODO: make configurable + const arrayLength = 3; + + if ('mongoType' in elementType) { + // Array of primitives + const fakerCall = generateFakerCall(elementType as FieldMapping); + return `Array.from({length: ${arrayLength}}, () => ${fakerCall})`; + } else if ('type' in elementType && elementType.type === 'array') { + // Nested array (e.g., matrix[][]) + const nestedArrayCode = generateArrayCode(elementType as ArrayStructure); + return `Array.from({length: ${arrayLength}}, () => ${nestedArrayCode})`; + } else { + // Array of objects + const objectCode = generateDocumentCode(elementType as DocumentStructure); + return `Array.from({length: ${arrayLength}}, () => ${objectCode})`; + } +} + +/** + * Generate faker.js call from field mapping + */ +function generateFakerCall(mapping: FieldMapping): string { + // TODO: Handle arguments properly + return `faker.${mapping.fakerMethod}()`; +} From c56d0ade170eedb74b0289b4ce88680bdb23a73d Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Tue, 9 Sep 2025 17:57:50 -0400 Subject: [PATCH 02/31] WIP --- .../mock-data-generator-modal/script-generation-utils.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 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 b7b4116fc1b..9171b9fafef 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,7 +1,7 @@ import { expect } from 'chai'; import { generateScript, type FieldMapping } from './script-generation-utils'; -describe('Simple Script Generation', () => { +describe('Script Generation', () => { const createFieldMapping = (fakerMethod: string): FieldMapping => ({ mongoType: 'String', fakerMethod, @@ -194,7 +194,7 @@ describe('Simple Script Generation', () => { const result = generateScript(schema, { databaseName: 'testdb', - collectionName: 'simple', + collectionName: 'coll', documentCount: 1, }); From 4a65473b737fe1e725c12bf7e6f3afc1d7b23738 Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Wed, 10 Sep 2025 12:07:11 -0400 Subject: [PATCH 03/31] Connect and upload --- .../script-generation-utils.ts | 35 +++++++++++++++---- 1 file changed, 28 insertions(+), 7 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 7f81983397c..73097ed48f7 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 @@ -222,23 +222,44 @@ export function generateScript( const documentCode = generateDocumentCode(structure); - const script = ` -// Generated Mock Data Script + // Escape ' and ` in database/collection names for template literals + const escapedDbName = options.databaseName + .replace(/'/g, "\\'") + .replace(/`/g, '\\`'); + const escapedCollectionName = options.collectionName + .replace(/'/g, "\\'") + .replace(/`/g, '\\`'); + + // Validate document count + const documentCount = Math.max( + 1, + Math.min(10000, Math.floor(options.documentCount)) + ); + + const script = `// Mock Data Generator Script +// Generated for collection: ${escapedDbName}.${escapedCollectionName} +// Document count: ${documentCount} + const { faker } = require('@faker-js/faker'); +// Connect to database +use('${escapedDbName}'); + +// Document generation function function generateDocument() { return ${documentCode}; } -// Generate documents +// Generate and insert documents const documents = []; -for (let i = 0; i < ${options.documentCount}; i++) { +for (let i = 0; i < ${documentCount}; i++) { documents.push(generateDocument()); } -// TODO: Add database connection and insertion -console.log('Generated', documents.length, 'documents'); -`; +// Insert documents into collection +db.getCollection('${escapedCollectionName}').insertMany(documents); + +console.log(\`Successfully inserted \${documents.length} documents into ${escapedDbName}.${escapedCollectionName}\`);`; return { script, From a6cc38dbb05641f61acb992ac1a5fb0c6af4bce2 Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Wed, 10 Sep 2025 12:21:00 -0400 Subject: [PATCH 04/31] Indentation --- .../script-generation-utils.spec.ts | 4 +- .../script-generation-utils.ts | 44 ++++++++++++++----- 2 files changed, 35 insertions(+), 13 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 9171b9fafef..4d01740d885 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 @@ -23,7 +23,7 @@ describe('Script Generation', () => { expect(result.success).to.equal(true); if (result.success) { - expect(result.script).to.contain('use("testdb")'); + expect(result.script).to.contain("use('testdb')"); expect(result.script).to.contain('faker.person.fullName()'); expect(result.script).to.contain('faker.internet.email()'); expect(result.script).to.contain('insertMany'); @@ -180,7 +180,7 @@ describe('Script Generation', () => { expect(result.success).to.equal(true); if (result.success) { - expect(result.script).to.contain('use("testdb")'); + expect(result.script).to.contain("use('testdb')"); expect(result.script).to.contain('insertMany'); // Should generate empty objects expect(result.script).to.contain('{}'); 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 73097ed48f7..b3750e26a0e 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 @@ -277,37 +277,53 @@ console.log(\`Successfully inserted \${documents.length} documents into ${escape /** * Generate JavaScript object code from document structure */ -function generateDocumentCode(structure: DocumentStructure): string { +function generateDocumentCode( + structure: DocumentStructure, + indent: number = 2 +): string { // For each field in structure: // - If FieldMapping: generate faker call // - If DocumentStructure: generate nested object // - If ArrayStructure: generate array - const parts: string[] = []; + const fieldIndent = ' '.repeat(indent); + const closingBraceIndent = ' '.repeat(indent - 2); + const rootLevelFields: string[] = []; for (const [fieldName, value] of Object.entries(structure)) { if ('mongoType' in value) { // It's a field mapping const fakerCall = generateFakerCall(value as FieldMapping); - parts.push(` ${fieldName}: ${fakerCall}`); + rootLevelFields.push(`${fieldIndent}${fieldName}: ${fakerCall}`); } else if ('type' in value && value.type === 'array') { // It's an array - const arrayCode = generateArrayCode(value as ArrayStructure); - parts.push(` ${fieldName}: ${arrayCode}`); + const arrayCode = generateArrayCode(value as ArrayStructure, indent + 2); + rootLevelFields.push(`${fieldIndent}${fieldName}: ${arrayCode}`); } else { // It's a nested object: recursive call - const nestedCode = generateDocumentCode(value as DocumentStructure); - parts.push(` ${fieldName}: ${nestedCode}`); + const nestedCode = generateDocumentCode( + value as DocumentStructure, + indent + 2 + ); + rootLevelFields.push(`${fieldIndent}${fieldName}: ${nestedCode}`); } } - return `{\n${parts.join(',\n')}\n}`; + // Handle empty objects + if (rootLevelFields.length === 0) { + return '{}'; + } + + return `{\n${rootLevelFields.join(',\n')}\n${closingBraceIndent}}`; } /** * Generate array code */ -function generateArrayCode(arrayStructure: ArrayStructure): string { +function generateArrayCode( + arrayStructure: ArrayStructure, + indent: number = 2 +): string { const elementType = arrayStructure.elementType; // Fixed length for now - TODO: make configurable @@ -319,11 +335,17 @@ function generateArrayCode(arrayStructure: ArrayStructure): string { return `Array.from({length: ${arrayLength}}, () => ${fakerCall})`; } else if ('type' in elementType && elementType.type === 'array') { // Nested array (e.g., matrix[][]) - const nestedArrayCode = generateArrayCode(elementType as ArrayStructure); + const nestedArrayCode = generateArrayCode( + elementType as ArrayStructure, + indent + ); return `Array.from({length: ${arrayLength}}, () => ${nestedArrayCode})`; } else { // Array of objects - const objectCode = generateDocumentCode(elementType as DocumentStructure); + const objectCode = generateDocumentCode( + elementType as DocumentStructure, + indent + ); return `Array.from({length: ${arrayLength}}, () => ${objectCode})`; } } From dfacd1678523b7671443a13aa19e6b3327610e39 Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Wed, 10 Sep 2025 16:59:03 -0400 Subject: [PATCH 05/31] Array lengths --- .../script-generation-utils.spec.ts | 114 ++++++++++++++++++ .../script-generation-utils.ts | 57 +++++++-- 2 files changed, 160 insertions(+), 11 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 4d01740d885..2914a69fdec 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 @@ -232,4 +232,118 @@ describe('Script Generation', () => { } }); }); + + describe('Configurable Array Lengths', () => { + it('should use default array length when no map provided', () => { + const schema = { + 'tags[]': createFieldMapping('lorem.word'), + }; + + const result = generateScript(schema, { + databaseName: 'testdb', + collectionName: 'posts', + documentCount: 1, + }); + + expect(result.success).to.equal(true); + if (result.success) { + expect(result.script).to.contain('Array.from({length: 3}'); + } + }); + + it('should use custom array length from map', () => { + const schema = { + 'tags[]': createFieldMapping('lorem.word'), + }; + + const result = generateScript(schema, { + databaseName: 'testdb', + collectionName: 'posts', + documentCount: 1, + arrayLengthMap: { + tags: 5, + }, + }); + + expect(result.success).to.equal(true); + if (result.success) { + expect(result.script).to.contain('Array.from({length: 5}'); + } + }); + + it('should handle nested array length configuration', () => { + const schema = { + 'users[].tags[]': createFieldMapping('lorem.word'), + }; + + const result = generateScript(schema, { + databaseName: 'testdb', + collectionName: 'groups', + documentCount: 1, + arrayLengthMap: { + users: { + tags: 4, + }, + }, + }); + + expect(result.success).to.equal(true); + if (result.success) { + // Should have tags array with length 4 + expect(result.script).to.contain('Array.from({length: 4}'); + } + }); + + it('should handle hierarchical array length map', () => { + const schema = { + 'departments[].employees[].skills[]': createFieldMapping('lorem.word'), + }; + + const result = generateScript(schema, { + databaseName: 'testdb', + collectionName: 'company', + documentCount: 1, + arrayLengthMap: { + departments: { + employees: { + skills: 3, + }, + }, + }, + }); + + expect(result.success).to.equal(true); + if (result.success) { + expect(result.script).to.contain('Array.from({length: 3}'); + } + }); + + it('should handle zero-length arrays', () => { + const schema = { + 'tags[]': createFieldMapping('lorem.word'), + 'categories[]': createFieldMapping('lorem.word'), + }; + + const result = generateScript(schema, { + databaseName: 'testdb', + collectionName: 'posts', + documentCount: 1, + arrayLengthMap: { + tags: 0, + categories: 2, + }, + }); + + expect(result.success).to.equal(true); + if (result.success) { + // Should have tags array with length 0 (empty array) + expect(result.script).to.contain('Array.from({length: 0}'); + // Should have categories array with length 2 + expect(result.script).to.contain('Array.from({length: 2}'); + // Verify both arrays are present + expect(result.script).to.contain('tags:'); + expect(result.script).to.contain('categories:'); + } + }); + }); }); 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 b3750e26a0e..58b06d26c02 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 @@ -4,11 +4,16 @@ export interface FieldMapping { fakerArgs: any[]; // TODO: type this properly later } +// Hierarchical array length map that mirrors document structure +export type ArrayLengthMap = { + [fieldName: string]: number | ArrayLengthMap; +}; + export interface ScriptOptions { documentCount: number; databaseName: string; collectionName: string; - // TODO: array lengths - for now use fixed length + arrayLengthMap?: ArrayLengthMap; } export interface ScriptResult { @@ -220,7 +225,11 @@ export function generateScript( try { const structure = buildDocumentStructure(schema); - const documentCode = generateDocumentCode(structure); + const documentCode = generateDocumentCode( + structure, + 2, + options.arrayLengthMap || {} + ); // Escape ' and ` in database/collection names for template literals const escapedDbName = options.databaseName @@ -233,7 +242,7 @@ export function generateScript( // Validate document count const documentCount = Math.max( 1, - Math.min(10000, Math.floor(options.documentCount)) + Math.min(10000, Math.floor(options.documentCount)) // TODO ); const script = `// Mock Data Generator Script @@ -279,7 +288,8 @@ console.log(\`Successfully inserted \${documents.length} documents into ${escape */ function generateDocumentCode( structure: DocumentStructure, - indent: number = 2 + indent: number = 2, + arrayLengthMap: ArrayLengthMap = {} ): string { // For each field in structure: // - If FieldMapping: generate faker call @@ -297,13 +307,28 @@ function generateDocumentCode( rootLevelFields.push(`${fieldIndent}${fieldName}: ${fakerCall}`); } else if ('type' in value && value.type === 'array') { // It's an array - const arrayCode = generateArrayCode(value as ArrayStructure, indent + 2); + const nestedArrayLengthMap = + typeof arrayLengthMap[fieldName] === 'object' + ? arrayLengthMap[fieldName] + : {}; + const arrayCode = generateArrayCode( + value as ArrayStructure, + indent + 2, + fieldName, + arrayLengthMap, + nestedArrayLengthMap + ); rootLevelFields.push(`${fieldIndent}${fieldName}: ${arrayCode}`); } else { // It's a nested object: recursive call + const nestedArrayLengthMap = + typeof arrayLengthMap[fieldName] === 'object' + ? arrayLengthMap[fieldName] + : arrayLengthMap; const nestedCode = generateDocumentCode( value as DocumentStructure, - indent + 2 + indent + 2, + nestedArrayLengthMap ); rootLevelFields.push(`${fieldIndent}${fieldName}: ${nestedCode}`); } @@ -322,12 +347,18 @@ function generateDocumentCode( */ function generateArrayCode( arrayStructure: ArrayStructure, - indent: number = 2 + indent: number = 2, + fieldName: string = '', + parentArrayLengthMap: ArrayLengthMap = {}, + nestedArrayLengthMap: ArrayLengthMap = {} ): string { const elementType = arrayStructure.elementType; - // Fixed length for now - TODO: make configurable - const arrayLength = 3; + // Get array length from map or use default + const arrayLength = + typeof parentArrayLengthMap[fieldName] === 'number' + ? parentArrayLengthMap[fieldName] + : 3; if ('mongoType' in elementType) { // Array of primitives @@ -337,14 +368,18 @@ function generateArrayCode( // Nested array (e.g., matrix[][]) const nestedArrayCode = generateArrayCode( elementType as ArrayStructure, - indent + indent, + '', // No specific field name for nested arrays + nestedArrayLengthMap, + {} ); return `Array.from({length: ${arrayLength}}, () => ${nestedArrayCode})`; } else { // Array of objects const objectCode = generateDocumentCode( elementType as DocumentStructure, - indent + indent, + nestedArrayLengthMap ); return `Array.from({length: ${arrayLength}}, () => ${objectCode})`; } From a0ca7c9a33a7c65dd2b5e1b252725d5e58c6d6a3 Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Wed, 10 Sep 2025 17:05:56 -0400 Subject: [PATCH 06/31] Unrecognized --- .../script-generation-utils.spec.ts | 149 ++++++++++++++++++ .../script-generation-utils.ts | 34 +++- 2 files changed, 182 insertions(+), 1 deletion(-) 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 2914a69fdec..89650a1576c 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 @@ -346,4 +346,153 @@ describe('Script Generation', () => { } }); }); + + describe('Unrecognized Field Defaults', () => { + it('should use default faker method for unrecognized string fields', () => { + const schema = { + unknownField: { + mongoType: 'string', + fakerMethod: 'unrecognized', + fakerArgs: [], + }, + }; + + const result = generateScript(schema, { + databaseName: 'testdb', + collectionName: 'test', + documentCount: 1, + }); + + expect(result.success).to.equal(true); + if (result.success) { + expect(result.script).to.contain('faker.lorem.word()'); + } + }); + + it('should use default faker method for unrecognized number fields', () => { + const schema = { + unknownNumber: { + mongoType: 'number', + fakerMethod: 'unrecognized', + fakerArgs: [], + }, + }; + + const result = generateScript(schema, { + databaseName: 'testdb', + collectionName: 'test', + documentCount: 1, + }); + + expect(result.success).to.equal(true); + if (result.success) { + expect(result.script).to.contain('faker.number.int()'); + } + }); + + it('should use default faker method for unrecognized date fields', () => { + const schema = { + unknownDate: { + mongoType: 'date', + fakerMethod: 'unrecognized', + fakerArgs: [], + }, + }; + + const result = generateScript(schema, { + databaseName: 'testdb', + collectionName: 'test', + documentCount: 1, + }); + + expect(result.success).to.equal(true); + if (result.success) { + expect(result.script).to.contain('faker.date.recent()'); + } + }); + + it('should use default faker method for unrecognized boolean fields', () => { + const schema = { + unknownBool: { + mongoType: 'boolean', + fakerMethod: 'unrecognized', + fakerArgs: [], + }, + }; + + const result = generateScript(schema, { + databaseName: 'testdb', + collectionName: 'test', + documentCount: 1, + }); + + expect(result.success).to.equal(true); + if (result.success) { + expect(result.script).to.contain('faker.datatype.boolean()'); + } + }); + + it('should use default faker method for unrecognized ObjectId fields', () => { + const schema = { + unknownId: { + mongoType: 'objectid', + fakerMethod: 'unrecognized', + fakerArgs: [], + }, + }; + + const result = generateScript(schema, { + databaseName: 'testdb', + collectionName: 'test', + documentCount: 1, + }); + + expect(result.success).to.equal(true); + if (result.success) { + expect(result.script).to.contain('faker.database.mongodbObjectId()'); + } + }); + + it('should use default faker method for unrecognized double fields', () => { + const schema = { + unknownDouble: { + mongoType: 'double', + fakerMethod: 'unrecognized', + fakerArgs: [], + }, + }; + + const result = generateScript(schema, { + databaseName: 'testdb', + collectionName: 'test', + documentCount: 1, + }); + + expect(result.success).to.equal(true); + if (result.success) { + expect(result.script).to.contain('faker.number.float()'); + } + }); + + it('should fall back to lorem.word for unknown MongoDB types', () => { + const schema = { + unknownType: { + mongoType: 'unknownType', + fakerMethod: 'unrecognized', + fakerArgs: [], + }, + }; + + const result = generateScript(schema, { + databaseName: 'testdb', + collectionName: 'test', + documentCount: 1, + }); + + expect(result.success).to.equal(true); + if (result.success) { + expect(result.script).to.contain('faker.lorem.word()'); + } + }); + }); }); 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 58b06d26c02..faddbca0eea 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 @@ -389,6 +389,38 @@ function generateArrayCode( * Generate faker.js call from field mapping */ function generateFakerCall(mapping: FieldMapping): string { + const method = + mapping.fakerMethod === 'unrecognized' + ? getDefaultFakerMethod(mapping.mongoType) + : mapping.fakerMethod; + // TODO: Handle arguments properly - return `faker.${mapping.fakerMethod}()`; + return `faker.${method}()`; +} + +/** + * Gets default faker method for unrecognized fields based on MongoDB type + */ +export function getDefaultFakerMethod(mongoType: string): string { + switch (mongoType.toLowerCase()) { + case 'string': + return 'lorem.word'; + case 'number': + case 'int32': + case 'int64': + return 'number.int'; + case 'double': + case 'decimal128': + return 'number.float'; + case 'date': + return 'date.recent'; + case 'objectid': + return 'database.mongodbObjectId'; + case 'boolean': + return 'datatype.boolean'; + case 'binary': + return 'string.hexadecimal'; + default: + return 'lorem.word'; + } } From 49d7b1cbc543cbe79213753d78523529c97cd232 Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Wed, 10 Sep 2025 17:39:51 -0400 Subject: [PATCH 07/31] Faker Args --- .../script-generation-utils.spec.ts | 176 ++++++++++++++++++ .../script-generation-utils.ts | 41 ++-- 2 files changed, 206 insertions(+), 11 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 89650a1576c..36c51012a47 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 @@ -495,4 +495,180 @@ describe('Script Generation', () => { } }); }); + + describe('Faker Arguments', () => { + it('should handle string arguments', () => { + const schema = { + name: { + mongoType: 'string', + fakerMethod: 'person.firstName', + fakerArgs: ['male'], + }, + }; + + const result = generateScript(schema, { + databaseName: 'testdb', + collectionName: 'users', + documentCount: 1, + }); + + expect(result.success).to.equal(true); + if (result.success) { + expect(result.script).to.contain("faker.person.firstName('male')"); + } + }); + + it('should handle number arguments', () => { + const schema = { + age: { + mongoType: 'number', + fakerMethod: 'number.int', + fakerArgs: [18, 65], + }, + }; + + const result = generateScript(schema, { + databaseName: 'testdb', + collectionName: 'users', + documentCount: 1, + }); + + expect(result.success).to.equal(true); + if (result.success) { + expect(result.script).to.contain('faker.number.int(18, 65)'); + } + }); + + it('should handle boolean arguments', () => { + const schema = { + active: { + mongoType: 'boolean', + fakerMethod: 'datatype.boolean', + fakerArgs: [0.8], + }, + }; + + const result = generateScript(schema, { + databaseName: 'testdb', + collectionName: 'users', + documentCount: 1, + }); + + expect(result.success).to.equal(true); + if (result.success) { + expect(result.script).to.contain('faker.datatype.boolean(0.8)'); + } + }); + + it('should handle JSON object arguments', () => { + const schema = { + score: { + mongoType: 'number', + fakerMethod: 'number.int', + fakerArgs: [{ json: '{"min":0,"max":100}' }], + }, + }; + + const result = generateScript(schema, { + databaseName: 'testdb', + collectionName: 'tests', + documentCount: 1, + }); + + expect(result.success).to.equal(true); + if (result.success) { + expect(result.script).to.contain( + 'faker.number.int({"min":0,"max":100})' + ); + } + }); + + it('should handle JSON arguments', () => { + const schema = { + color: { + mongoType: 'string', + fakerMethod: 'helpers.arrayElement', + fakerArgs: [{ json: "['red', 'blue', 'green']" }], + }, + }; + + const result = generateScript(schema, { + databaseName: 'testdb', + collectionName: 'items', + documentCount: 1, + }); + + expect(result.success).to.equal(true); + if (result.success) { + expect(result.script).to.contain( + "faker.helpers.arrayElement(['red', 'blue', 'green'])" + ); + } + }); + + it('should handle mixed argument types', () => { + const schema = { + description: { + mongoType: 'string', + fakerMethod: 'lorem.words', + fakerArgs: [5, true], + }, + }; + + const result = generateScript(schema, { + databaseName: 'testdb', + collectionName: 'posts', + documentCount: 1, + }); + + expect(result.success).to.equal(true); + if (result.success) { + expect(result.script).to.contain('faker.lorem.words(5, true)'); + } + }); + + it('should escape quotes in string arguments', () => { + const schema = { + quote: { + mongoType: 'string', + fakerMethod: 'lorem.sentence', + fakerArgs: ["It's a 'test' string"], + }, + }; + + const result = generateScript(schema, { + databaseName: 'testdb', + collectionName: 'quotes', + documentCount: 1, + }); + + expect(result.success).to.equal(true); + if (result.success) { + expect(result.script).to.contain( + "faker.lorem.sentence('It\\'s a \\'test\\' string')" + ); + } + }); + + it('should handle empty arguments array', () => { + const schema = { + id: { + mongoType: 'string', + fakerMethod: 'string.uuid', + fakerArgs: [], + }, + }; + + const result = generateScript(schema, { + databaseName: 'testdb', + collectionName: 'items', + documentCount: 1, + }); + + expect(result.success).to.equal(true); + if (result.success) { + expect(result.script).to.contain('faker.string.uuid()'); + } + }); + }); }); 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 faddbca0eea..5f27493d279 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,9 @@ +export type FakerArg = string | number | boolean | { json: string }; + export interface FieldMapping { mongoType: string; fakerMethod: string; - fakerArgs: any[]; // TODO: type this properly later + fakerArgs: FakerArg[]; } // Hierarchical array length map that mirrors document structure @@ -239,15 +241,9 @@ export function generateScript( .replace(/'/g, "\\'") .replace(/`/g, '\\`'); - // Validate document count - const documentCount = Math.max( - 1, - Math.min(10000, Math.floor(options.documentCount)) // TODO - ); - const script = `// Mock Data Generator Script // Generated for collection: ${escapedDbName}.${escapedCollectionName} -// Document count: ${documentCount} +// Document count: ${options.documentCount} const { faker } = require('@faker-js/faker'); @@ -261,7 +257,7 @@ function generateDocument() { // Generate and insert documents const documents = []; -for (let i = 0; i < ${documentCount}; i++) { +for (let i = 0; i < ${options.documentCount}; i++) { documents.push(generateDocument()); } @@ -394,8 +390,8 @@ function generateFakerCall(mapping: FieldMapping): string { ? getDefaultFakerMethod(mapping.mongoType) : mapping.fakerMethod; - // TODO: Handle arguments properly - return `faker.${method}()`; + const args = formatFakerArgs(mapping.fakerArgs); + return `faker.${method}(${args})`; } /** @@ -424,3 +420,26 @@ export function getDefaultFakerMethod(mongoType: string): string { return 'lorem.word'; } } + +/** + * Converts faker arguments to JavaScript code + */ +export function formatFakerArgs(fakerArgs: FakerArg[]): string { + const stringifiedArgs: string[] = []; + + for (const arg of fakerArgs) { + if (typeof arg === 'string') { + // Escape single quotes for JS strings (and backticks for security) + const escapedArg = arg.replace(/[`']/g, '\\$&'); + stringifiedArgs.push(`'${escapedArg}'`); + } else if (typeof arg === 'number' || typeof arg === 'boolean') { + stringifiedArgs.push(`${arg}`); + } else if (typeof arg === 'object' && arg !== null && 'json' in arg) { + // Pre-serialized JSON objects + const jsonArg = arg as { json: string }; + stringifiedArgs.push(jsonArg.json); + } + } + + return stringifiedArgs.join(', '); +} From 662cf634a092f10f8cabf6d3a1e2450926db1c34 Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Wed, 10 Sep 2025 18:07:57 -0400 Subject: [PATCH 08/31] Probability --- .../script-generation-utils.spec.ts | 170 +++++++++++++++++- .../script-generation-utils.ts | 16 +- 2 files changed, 183 insertions(+), 3 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 36c51012a47..7ff83685edc 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 @@ -2,10 +2,14 @@ import { expect } from 'chai'; import { generateScript, type FieldMapping } from './script-generation-utils'; describe('Script Generation', () => { - const createFieldMapping = (fakerMethod: string): FieldMapping => ({ + const createFieldMapping = ( + fakerMethod: string, + probability?: number + ): FieldMapping => ({ mongoType: 'String', fakerMethod, fakerArgs: [], + ...(probability !== undefined && { probability }), }); describe('Basic field generation', () => { @@ -671,4 +675,168 @@ describe('Script Generation', () => { } }); }); + + describe('Probability Handling', () => { + it('should generate normal faker call for probability 1.0', () => { + const schema = { + name: createFieldMapping('person.fullName', 1.0), + }; + + const result = generateScript(schema, { + databaseName: 'testdb', + collectionName: 'users', + documentCount: 1, + }); + + expect(result.success).to.equal(true); + if (result.success) { + expect(result.script).to.contain('faker.person.fullName()'); + expect(result.script).not.to.contain('Math.random()'); + } + }); + + it('should generate normal faker call when probability is undefined', () => { + const schema = { + name: createFieldMapping('person.fullName'), // No probability specified + }; + + const result = generateScript(schema, { + databaseName: 'testdb', + collectionName: 'users', + documentCount: 1, + }); + + expect(result.success).to.equal(true); + if (result.success) { + expect(result.script).to.contain('faker.person.fullName()'); + expect(result.script).not.to.contain('Math.random()'); + } + }); + + it('should use probabilistic rendering when probability < 1.0', () => { + const schema = { + optionalField: createFieldMapping('lorem.word', 0.7), + }; + + const result = generateScript(schema, { + databaseName: 'testdb', + collectionName: 'posts', + documentCount: 1, + }); + + expect(result.success).to.equal(true); + if (result.success) { + expect(result.script).to.contain( + '...(Math.random() < 0.7 ? { optionalField: faker.lorem.word() } : {})' + ); + } + }); + + it('should handle zero probability', () => { + const schema = { + rareField: createFieldMapping('lorem.word', 0), + }; + + const result = generateScript(schema, { + databaseName: 'testdb', + collectionName: 'posts', + documentCount: 1, + }); + + expect(result.success).to.equal(true); + if (result.success) { + expect(result.script).to.contain( + '...(Math.random() < 0 ? { rareField: faker.lorem.word() } : {})' + ); + } + }); + + it('should handle mixed probability fields', () => { + const schema = { + alwaysPresent: createFieldMapping('person.fullName', 1.0), + sometimesPresent: createFieldMapping('internet.email', 0.8), + rarelyPresent: createFieldMapping('phone.number', 0.2), + defaultProbability: createFieldMapping('lorem.word'), // undefined = 1.0 + }; + + const result = generateScript(schema, { + databaseName: 'testdb', + collectionName: 'users', + documentCount: 1, + }); + + expect(result.success).to.equal(true); + if (result.success) { + // Always present (probability 1.0) + expect(result.script).to.contain('faker.person.fullName()'); + expect(result.script).not.to.contain( + 'Math.random() < 1 ? { alwaysPresent:' + ); + + // Sometimes present (probability 0.8) + expect(result.script).to.contain( + '...(Math.random() < 0.8 ? { sometimesPresent: faker.internet.email() } : {})' + ); + + // Rarely present (probability 0.2) + expect(result.script).to.contain( + '...(Math.random() < 0.2 ? { rarelyPresent: faker.phone.number() } : {})' + ); + + // Default probability (undefined = 1.0) + expect(result.script).to.contain('faker.lorem.word()'); + expect(result.script).not.to.contain( + 'Math.random() < 1 ? { defaultProbability:' + ); + } + }); + + it('should handle probability with faker arguments', () => { + const schema = { + conditionalAge: { + mongoType: 'number', + fakerMethod: 'number.int', + fakerArgs: [18, 65], + probability: 0.9, + }, + }; + + const result = generateScript(schema, { + databaseName: 'testdb', + collectionName: 'users', + documentCount: 1, + }); + + expect(result.success).to.equal(true); + if (result.success) { + expect(result.script).to.contain( + '...(Math.random() < 0.9 ? { conditionalAge: faker.number.int(18, 65) } : {})' + ); + } + }); + + it('should handle probability with unrecognized fields', () => { + const schema = { + unknownField: { + mongoType: 'string', + fakerMethod: 'unrecognized', + fakerArgs: [], + probability: 0.5, + }, + }; + + const result = generateScript(schema, { + databaseName: 'testdb', + collectionName: 'test', + documentCount: 1, + }); + + expect(result.success).to.equal(true); + if (result.success) { + expect(result.script).to.contain( + '...(Math.random() < 0.5 ? { unknownField: faker.lorem.word() } : {})' + ); + } + }); + }); }); 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 5f27493d279..d843865b09c 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 @@ -4,6 +4,7 @@ export interface FieldMapping { mongoType: string; fakerMethod: string; fakerArgs: FakerArg[]; + probability?: number; // 0.0 - 1.0 frequency of field (defaults to 1.0) } // Hierarchical array length map that mirrors document structure @@ -299,8 +300,19 @@ function generateDocumentCode( for (const [fieldName, value] of Object.entries(structure)) { if ('mongoType' in value) { // It's a field mapping - const fakerCall = generateFakerCall(value as FieldMapping); - rootLevelFields.push(`${fieldIndent}${fieldName}: ${fakerCall}`); + const mapping = value as FieldMapping; + const fakerCall = generateFakerCall(mapping); + const probability = mapping.probability ?? 1.0; + + if (probability < 1.0) { + // Use Math.random for conditional field inclusion + rootLevelFields.push( + `${fieldIndent}...(Math.random() < ${probability} ? { ${fieldName}: ${fakerCall} } : {})` + ); + } else { + // Normal field inclusion + rootLevelFields.push(`${fieldIndent}${fieldName}: ${fakerCall}`); + } } else if ('type' in value && value.type === 'array') { // It's an array const nestedArrayLengthMap = From a8f6aacb72f08fad410bb5829e917d24ed161542 Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Thu, 11 Sep 2025 16:00:26 -0400 Subject: [PATCH 09/31] More Validation --- .../script-generation-utils.spec.ts | 113 ++++++++++++++++++ .../script-generation-utils.ts | 74 +++++++++--- 2 files changed, 173 insertions(+), 14 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 7ff83685edc..5cd3720d07c 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 @@ -839,4 +839,117 @@ describe('Script Generation', () => { } }); }); + + describe('Graceful Handling', () => { + it('should default invalid probability to 1.0', () => { + const schema = { + field1: { + mongoType: 'string', + fakerMethod: 'lorem.word', + fakerArgs: [], + probability: 1.5, // Invalid - should default to 1.0 + }, + field2: { + mongoType: 'string', + fakerMethod: 'lorem.word', + fakerArgs: [], + probability: -0.5, // Invalid - should default to 1.0 + }, + field3: { + mongoType: 'string', + fakerMethod: 'lorem.word', + fakerArgs: [], + probability: 'invalid' as any, // Invalid - should default to 1.0 + }, + }; + + const result = generateScript(schema, { + databaseName: 'test', + collectionName: 'test', + documentCount: 1, + }); + + expect(result.success).to.equal(true); + if (result.success) { + // All fields should be treated as probability 1.0 (always present) + expect(result.script).to.contain('field1: faker.lorem.word()'); + expect(result.script).to.contain('field2: faker.lorem.word()'); + expect(result.script).to.contain('field3: faker.lorem.word()'); + expect(result.script).not.to.contain('Math.random()'); + } + }); + + it('should handle field names with brackets (non-array)', () => { + const schema = { + 'settings[theme]': createFieldMapping('lorem.word'), + 'data[0]': createFieldMapping('lorem.word'), + 'bracket]field': createFieldMapping('lorem.word'), + '[metadata': createFieldMapping('lorem.word'), + }; + + const result = generateScript(schema, { + databaseName: 'test', + collectionName: 'test', + documentCount: 1, + }); + + expect(result.success).to.equal(true); + if (result.success) { + // All fields should be treated as regular field names, not arrays + expect(result.script).to.contain('settings[theme]: faker.lorem.word()'); + expect(result.script).to.contain('data[0]: faker.lorem.word()'); + expect(result.script).to.contain('bracket]field: faker.lorem.word()'); + expect(result.script).to.contain('[metadata: faker.lorem.word()'); + expect(result.script).not.to.contain('Array.from'); + } + }); + + it('should handle field names with [] in middle (not array notation)', () => { + const schema = { + 'squareBrackets[]InMiddle': createFieldMapping('lorem.word'), + 'field[]WithMore': createFieldMapping('lorem.word'), + 'start[]middle[]end': createFieldMapping('lorem.word'), + }; + + const result = generateScript(schema, { + databaseName: 'test', + collectionName: 'test', + documentCount: 1, + }); + + expect(result.success).to.equal(true); + if (result.success) { + // These should be treated as regular field names, not arrays + expect(result.script).to.contain( + 'squareBrackets[]InMiddle: faker.lorem.word()' + ); + expect(result.script).to.contain('field[]WithMore: faker.lorem.word()'); + expect(result.script).to.contain( + 'start[]middle[]end: faker.lorem.word()' + ); + expect(result.script).not.to.contain('Array.from'); + } + }); + + it('should still handle real array notation correctly', () => { + const schema = { + 'realArray[]': createFieldMapping('lorem.word'), + 'nestedArray[].field': createFieldMapping('lorem.word'), + }; + + const result = generateScript(schema, { + databaseName: 'test', + collectionName: 'test', + documentCount: 1, + }); + + expect(result.success).to.equal(true); + if (result.success) { + // These should be treated as arrays + expect(result.script).to.contain('Array.from'); + expect(result.script).to.contain('realArray: Array.from'); + expect(result.script).to.contain('nestedArray: Array.from'); + } + }); + }); }); 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 d843865b09c..c5c8aef66ab 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 @@ -58,14 +58,34 @@ function parseFieldPath(fieldPath: string): string[] { if (current) { parts.push(current); current = ''; + } else if (parts.length > 0 && parts[parts.length - 1] === '[]') { + // This is valid: "users[].name" - dot after array notation + // Continue parsing + } else { + throw new Error( + `Invalid field path "${fieldPath}": empty field name before dot` + ); } } else if (char === '[' && fieldPath[i + 1] === ']') { - if (current) { - parts.push(current); - current = ''; + // Only treat [] as array notation if it's at the end, followed by a dot, or followed by another [ + const isAtEnd = i + 2 >= fieldPath.length; + const isFollowedByDot = + i + 2 < fieldPath.length && fieldPath[i + 2] === '.'; + const isFollowedByBracket = + i + 2 < fieldPath.length && fieldPath[i + 2] === '['; + + if (isAtEnd || isFollowedByDot || isFollowedByBracket) { + // This is array notation + if (current) { + parts.push(current); + current = ''; + } + parts.push('[]'); + i++; // Skip the ] + } else { + // This is just part of the field name + current += char; } - parts.push('[]'); - i++; // Skip the ] } else { current += char; } @@ -75,6 +95,12 @@ function parseFieldPath(fieldPath: string): string[] { parts.push(current); } + if (parts.length === 0) { + throw new Error( + `Invalid field path "${fieldPath}": no valid field names found` + ); + } + return parts; } /** @@ -103,18 +129,16 @@ function insertIntoStructure( mapping: FieldMapping ): void { if (pathParts.length === 0) { - // This shouldn't happen - // TODO: log error - return; + throw new Error('Cannot insert field mapping: empty path parts array'); } // Base case: insert root-level field mapping if (pathParts.length === 1) { const part = pathParts[0]; if (part === '[]') { - // This shouldn't happen - array without field name - // TODO: log error - return; + throw new Error( + 'Invalid field path: array notation "[]" cannot be used without a field name' + ); } structure[part] = mapping; return; @@ -302,7 +326,16 @@ function generateDocumentCode( // It's a field mapping const mapping = value as FieldMapping; const fakerCall = generateFakerCall(mapping); - const probability = mapping.probability ?? 1.0; + // Default to 1.0 for invalid probability values + let probability = 1.0; + if ( + mapping.probability !== undefined && + typeof mapping.probability === 'number' && + mapping.probability >= 0 && + mapping.probability <= 1 + ) { + probability = mapping.probability; + } if (probability < 1.0) { // Use Math.random for conditional field inclusion @@ -439,17 +472,30 @@ export function getDefaultFakerMethod(mongoType: string): string { export function formatFakerArgs(fakerArgs: FakerArg[]): string { const stringifiedArgs: string[] = []; - for (const arg of fakerArgs) { + for (let i = 0; i < fakerArgs.length; i++) { + const arg = fakerArgs[i]; + if (typeof arg === 'string') { // Escape single quotes for JS strings (and backticks for security) const escapedArg = arg.replace(/[`']/g, '\\$&'); stringifiedArgs.push(`'${escapedArg}'`); - } else if (typeof arg === 'number' || typeof arg === 'boolean') { + } else if (typeof arg === 'number') { + if (!Number.isFinite(arg)) { + throw new Error( + `Invalid number argument at index ${i}: must be a finite number` + ); + } + stringifiedArgs.push(`${arg}`); + } else if (typeof arg === 'boolean') { stringifiedArgs.push(`${arg}`); } else if (typeof arg === 'object' && arg !== null && 'json' in arg) { // Pre-serialized JSON objects const jsonArg = arg as { json: string }; stringifiedArgs.push(jsonArg.json); + } else { + throw new Error( + `Invalid argument type at index ${i}: expected string, number, boolean, or {json: string}` + ); } } From 0a011a35d6ef5d5b76e5814e3bbf1e2f4cbf2018 Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Thu, 11 Sep 2025 16:30:28 -0400 Subject: [PATCH 10/31] Extract constants --- .../script-generation-utils.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 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 c5c8aef66ab..a63584eab6b 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,5 +1,8 @@ export type FakerArg = string | number | boolean | { json: string }; +const DEFAULT_ARRAY_LENGTH = 3; +const INDENT_SIZE = 2; + export interface FieldMapping { mongoType: string; fakerMethod: string; @@ -254,7 +257,7 @@ export function generateScript( const documentCode = generateDocumentCode( structure, - 2, + INDENT_SIZE, options.arrayLengthMap || {} ); @@ -309,7 +312,7 @@ console.log(\`Successfully inserted \${documents.length} documents into ${escape */ function generateDocumentCode( structure: DocumentStructure, - indent: number = 2, + indent: number = INDENT_SIZE, arrayLengthMap: ArrayLengthMap = {} ): string { // For each field in structure: @@ -318,7 +321,7 @@ function generateDocumentCode( // - If ArrayStructure: generate array const fieldIndent = ' '.repeat(indent); - const closingBraceIndent = ' '.repeat(indent - 2); + const closingBraceIndent = ' '.repeat(indent - INDENT_SIZE); const rootLevelFields: string[] = []; for (const [fieldName, value] of Object.entries(structure)) { @@ -354,7 +357,7 @@ function generateDocumentCode( : {}; const arrayCode = generateArrayCode( value as ArrayStructure, - indent + 2, + indent + INDENT_SIZE, fieldName, arrayLengthMap, nestedArrayLengthMap @@ -368,7 +371,7 @@ function generateDocumentCode( : arrayLengthMap; const nestedCode = generateDocumentCode( value as DocumentStructure, - indent + 2, + indent + INDENT_SIZE, nestedArrayLengthMap ); rootLevelFields.push(`${fieldIndent}${fieldName}: ${nestedCode}`); @@ -388,7 +391,7 @@ function generateDocumentCode( */ function generateArrayCode( arrayStructure: ArrayStructure, - indent: number = 2, + indent: number = INDENT_SIZE, fieldName: string = '', parentArrayLengthMap: ArrayLengthMap = {}, nestedArrayLengthMap: ArrayLengthMap = {} @@ -399,7 +402,7 @@ function generateArrayCode( const arrayLength = typeof parentArrayLengthMap[fieldName] === 'number' ? parentArrayLengthMap[fieldName] - : 3; + : DEFAULT_ARRAY_LENGTH; if ('mongoType' in elementType) { // Array of primitives From 4cb73c350cc49871c1b6a42e5582205f18a3027c Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Fri, 12 Sep 2025 11:36:45 -0400 Subject: [PATCH 11/31] Tests are more readable --- .../script-generation-utils.spec.ts | 117 ++++++++---------- .../script-generation-utils.ts | 2 +- 2 files changed, 55 insertions(+), 64 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 5cd3720d07c..0a850b42ed3 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 @@ -27,9 +27,12 @@ describe('Script Generation', () => { expect(result.success).to.equal(true); if (result.success) { + const expectedReturnBlock = `return { + name: faker.person.fullName(), + email: faker.internet.email() + };`; + expect(result.script).to.contain(expectedReturnBlock); expect(result.script).to.contain("use('testdb')"); - expect(result.script).to.contain('faker.person.fullName()'); - expect(result.script).to.contain('faker.internet.email()'); expect(result.script).to.contain('insertMany'); } }); @@ -49,8 +52,10 @@ describe('Script Generation', () => { expect(result.success).to.equal(true); if (result.success) { - expect(result.script).to.contain('Array.from'); - expect(result.script).to.contain('faker.lorem.word()'); + const expectedReturnBlock = `return { + tags: Array.from({length: 3}, () => faker.lorem.word()) + };`; + expect(result.script).to.contain(expectedReturnBlock); } }); @@ -68,12 +73,14 @@ describe('Script Generation', () => { expect(result.success).to.equal(true); if (result.success) { - expect(result.script).to.contain('Array.from'); - expect(result.script).to.contain('faker.person.fullName()'); - expect(result.script).to.contain('faker.internet.email()'); - // Should have nested object structure - expect(result.script).to.match(/name:\s*faker\.person\.fullName\(\)/); - expect(result.script).to.match(/email:\s*faker\.internet\.email\(\)/); + // Should generate the complete return block with proper structure + const expectedReturnBlock = `return { + users: Array.from({length: 3}, () => { + name: faker.person.fullName(), + email: faker.internet.email() + }) + };`; + expect(result.script).to.contain(expectedReturnBlock); } }); @@ -90,12 +97,10 @@ describe('Script Generation', () => { expect(result.success).to.equal(true); if (result.success) { - // Should have nested Array.from calls - expect(result.script).to.contain('Array.from'); - expect(result.script).to.contain('faker.number.int()'); - // Should have two levels of Array.from for 2D array - const arrayFromMatches = result.script.match(/Array\.from/g); - expect(arrayFromMatches?.length).to.be.greaterThanOrEqual(2); + const expectedReturnBlock = `return { + matrix: Array.from({length: 3}, () => Array.from({length: 3}, () => faker.number.int())) + };`; + expect(result.script).to.contain(expectedReturnBlock); } }); @@ -113,11 +118,13 @@ describe('Script Generation', () => { expect(result.success).to.equal(true); if (result.success) { - expect(result.script).to.contain('faker.person.fullName()'); - expect(result.script).to.contain('faker.lorem.word()'); - // Should have nested structure: users array containing objects with tags arrays - expect(result.script).to.match(/name:\s*faker\.person\.fullName\(\)/); - expect(result.script).to.match(/tags:\s*Array\.from/); + const expectedReturnBlock = `return { + users: Array.from({length: 3}, () => { + name: faker.person.fullName(), + tags: Array.from({length: 3}, () => faker.lorem.word()) + }) + };`; + expect(result.script).to.contain(expectedReturnBlock); } }); }); @@ -139,11 +146,17 @@ describe('Script Generation', () => { expect(result.success).to.equal(true); if (result.success) { - expect(result.script).to.contain('faker.location.streetAddress()'); - expect(result.script).to.contain('faker.location.city()'); - // Should have nested object structure - expect(result.script).to.contain('profile'); - expect(result.script).to.contain('address'); + const expectedReturnBlock = `return { + users: Array.from({length: 3}, () => { + profile: { + address: { + street: faker.location.streetAddress(), + city: faker.location.city() + } + } + }) + };`; + expect(result.script).to.contain(expectedReturnBlock); } }); @@ -163,10 +176,15 @@ describe('Script Generation', () => { expect(result.success).to.equal(true); if (result.success) { - expect(result.script).to.contain('faker.lorem.sentence()'); - expect(result.script).to.contain('faker.person.fullName()'); - expect(result.script).to.contain('faker.lorem.words()'); - expect(result.script).to.contain('faker.date.recent()'); + const expectedReturnBlock = `return { + title: faker.lorem.sentence(), + authors: Array.from({length: 3}, () => { + name: faker.person.fullName(), + books: Array.from({length: 3}, () => faker.lorem.words()) + }), + publishedYear: faker.date.recent() + };`; + expect(result.script).to.contain(expectedReturnBlock); } }); }); @@ -184,10 +202,8 @@ describe('Script Generation', () => { expect(result.success).to.equal(true); if (result.success) { - expect(result.script).to.contain("use('testdb')"); - expect(result.script).to.contain('insertMany'); - // Should generate empty objects - expect(result.script).to.contain('{}'); + const expectedReturnBlock = `return {};`; + expect(result.script).to.contain(expectedReturnBlock); } }); @@ -204,35 +220,10 @@ describe('Script Generation', () => { expect(result.success).to.equal(true); if (result.success) { - expect(result.script).to.contain('faker.number.int()'); - expect(result.script).to.match(/value:\s*faker\.number\.int\(\)/); - } - }); - - it('should handle multiple fields in the same nested object', () => { - const schema = { - 'profile.name': createFieldMapping('person.fullName'), - 'profile.email': createFieldMapping('internet.email'), - 'profile.age': createFieldMapping('number.int'), - }; - - const result = generateScript(schema, { - databaseName: 'testdb', - collectionName: 'users', - documentCount: 1, - }); - - expect(result.success).to.equal(true); - if (result.success) { - // All three fields should be present in the same profile object - expect(result.script).to.contain('faker.person.fullName()'); - expect(result.script).to.contain('faker.internet.email()'); - expect(result.script).to.contain('faker.number.int()'); - expect(result.script).to.contain('profile'); - // Should have all fields in nested structure - expect(result.script).to.match(/name:\s*faker\.person\.fullName\(\)/); - expect(result.script).to.match(/email:\s*faker\.internet\.email\(\)/); - expect(result.script).to.match(/age:\s*faker\.number\.int\(\)/); + const expectedReturnBlock = `return { + value: faker.number.int() + };`; + expect(result.script).to.contain(expectedReturnBlock); } }); }); 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 a63584eab6b..5a668f61205 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 @@ -257,7 +257,7 @@ export function generateScript( const documentCode = generateDocumentCode( structure, - INDENT_SIZE, + INDENT_SIZE * 2, // 4 spaces: 2 for function body + 2 for inside return statement options.arrayLengthMap || {} ); From 36f7327ae760476ce560946c5d3b9032c15eb3d5 Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Fri, 12 Sep 2025 14:10:37 -0400 Subject: [PATCH 12/31] Array length map nesting --- .../script-generation-utils.spec.ts | 50 ++++++++++--------- .../script-generation-utils.ts | 44 ++++++++-------- 2 files changed, 50 insertions(+), 44 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 0a850b42ed3..e74ec8d316a 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 @@ -256,7 +256,7 @@ describe('Script Generation', () => { collectionName: 'posts', documentCount: 1, arrayLengthMap: { - tags: 5, + tags: [5], }, }); @@ -277,7 +277,7 @@ describe('Script Generation', () => { documentCount: 1, arrayLengthMap: { users: { - tags: 4, + tags: [4], }, }, }); @@ -289,55 +289,57 @@ describe('Script Generation', () => { } }); - it('should handle hierarchical array length map', () => { + it('should handle zero-length arrays', () => { const schema = { - 'departments[].employees[].skills[]': createFieldMapping('lorem.word'), + 'tags[]': createFieldMapping('lorem.word'), + 'categories[]': createFieldMapping('lorem.word'), }; const result = generateScript(schema, { databaseName: 'testdb', - collectionName: 'company', + collectionName: 'posts', documentCount: 1, arrayLengthMap: { - departments: { - employees: { - skills: 3, - }, - }, + tags: [0], + categories: [2], }, }); expect(result.success).to.equal(true); if (result.success) { - expect(result.script).to.contain('Array.from({length: 3}'); + // Should have tags array with length 0 (empty array) + expect(result.script).to.contain('Array.from({length: 0}'); + // Should have categories array with length 2 + expect(result.script).to.contain('Array.from({length: 2}'); + // Verify both arrays are present + expect(result.script).to.contain('tags:'); + expect(result.script).to.contain('categories:'); } }); - it('should handle zero-length arrays', () => { + it('should handle multi-dimensional arrays with custom lengths', () => { const schema = { - 'tags[]': createFieldMapping('lorem.word'), - 'categories[]': createFieldMapping('lorem.word'), + 'matrix[][]': createFieldMapping('number.int'), + 'cube[][][]': createFieldMapping('number.float'), }; const result = generateScript(schema, { databaseName: 'testdb', - collectionName: 'posts', + collectionName: 'data', documentCount: 1, arrayLengthMap: { - tags: 0, - categories: 2, + matrix: [2, 5], // 2x5 matrix + cube: [3, 4, 2], // 3x4x2 cube }, }); expect(result.success).to.equal(true); if (result.success) { - // Should have tags array with length 0 (empty array) - expect(result.script).to.contain('Array.from({length: 0}'); - // Should have categories array with length 2 - expect(result.script).to.contain('Array.from({length: 2}'); - // Verify both arrays are present - expect(result.script).to.contain('tags:'); - expect(result.script).to.contain('categories:'); + 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()))) + };`; + expect(result.script).to.contain(expectedReturnBlock); } }); }); 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 5a668f61205..31639339358 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 @@ -12,7 +12,7 @@ export interface FieldMapping { // Hierarchical array length map that mirrors document structure export type ArrayLengthMap = { - [fieldName: string]: number | ArrayLengthMap; + [fieldName: string]: number[] | ArrayLengthMap; }; export interface ScriptOptions { @@ -351,24 +351,24 @@ function generateDocumentCode( } } else if ('type' in value && value.type === 'array') { // It's an array - const nestedArrayLengthMap = - typeof arrayLengthMap[fieldName] === 'object' - ? arrayLengthMap[fieldName] - : {}; const arrayCode = generateArrayCode( value as ArrayStructure, indent + INDENT_SIZE, fieldName, arrayLengthMap, - nestedArrayLengthMap + 0 // Start at dimension 0 ); rootLevelFields.push(`${fieldIndent}${fieldName}: ${arrayCode}`); } else { // It's a nested object: recursive call + + // Type Validation/ Fallback for misconfigured array length map + const arrayDimensions = arrayLengthMap[fieldName]; const nestedArrayLengthMap = - typeof arrayLengthMap[fieldName] === 'object' - ? arrayLengthMap[fieldName] - : arrayLengthMap; + typeof arrayDimensions === 'object' && !Array.isArray(arrayDimensions) + ? arrayDimensions + : {}; + const nestedCode = generateDocumentCode( value as DocumentStructure, indent + INDENT_SIZE, @@ -393,33 +393,37 @@ function generateArrayCode( arrayStructure: ArrayStructure, indent: number = INDENT_SIZE, fieldName: string = '', - parentArrayLengthMap: ArrayLengthMap = {}, - nestedArrayLengthMap: ArrayLengthMap = {} + arrayLengthMap: ArrayLengthMap = {}, + dimensionIndex: number = 0 ): string { const elementType = arrayStructure.elementType; - // Get array length from map or use default - const arrayLength = - typeof parentArrayLengthMap[fieldName] === 'number' - ? parentArrayLengthMap[fieldName] - : DEFAULT_ARRAY_LENGTH; + // Get array length for this dimension + const arrayDimensions = arrayLengthMap[fieldName]; + const arrayLength = Array.isArray(arrayDimensions) + ? arrayDimensions[dimensionIndex] ?? DEFAULT_ARRAY_LENGTH + : DEFAULT_ARRAY_LENGTH; // Fallback for misconfigured array length map if ('mongoType' in elementType) { // Array of primitives const fakerCall = generateFakerCall(elementType as FieldMapping); return `Array.from({length: ${arrayLength}}, () => ${fakerCall})`; } else if ('type' in elementType && elementType.type === 'array') { - // Nested array (e.g., matrix[][]) + // Nested array (e.g., matrix[][]) - keep same fieldName, increment dimension const nestedArrayCode = generateArrayCode( elementType as ArrayStructure, indent, - '', // No specific field name for nested arrays - nestedArrayLengthMap, - {} + fieldName, + arrayLengthMap, + dimensionIndex + 1 // Next dimension ); return `Array.from({length: ${arrayLength}}, () => ${nestedArrayCode})`; } else { // Array of objects + const nestedArrayLengthMap = + typeof arrayDimensions === 'object' && !Array.isArray(arrayDimensions) + ? arrayDimensions + : {}; // Fallback for misconfigured array length map const objectCode = generateDocumentCode( elementType as DocumentStructure, indent, From 8e9dd61303623cacd1e4d225c60fcf5c1d50eff7 Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Fri, 12 Sep 2025 15:23:24 -0400 Subject: [PATCH 13/31] Update array map, reorg tests --- .../script-generation-utils.spec.ts | 490 +++++++++--------- .../script-generation-utils.ts | 40 +- 2 files changed, 260 insertions(+), 270 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 e74ec8d316a..72564009f7d 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 @@ -12,171 +12,136 @@ describe('Script Generation', () => { ...(probability !== undefined && { probability }), }); - describe('Basic field generation', () => { - it('should generate script for simple fields', () => { - const schema = { - name: createFieldMapping('person.fullName'), - email: createFieldMapping('internet.email'), - }; + it('should generate script for simple fields', () => { + const schema = { + name: createFieldMapping('person.fullName'), + email: createFieldMapping('internet.email'), + }; - const result = generateScript(schema, { - databaseName: 'testdb', - collectionName: 'users', - documentCount: 5, - }); + const result = generateScript(schema, { + databaseName: 'testdb', + collectionName: 'users', + documentCount: 5, + }); - expect(result.success).to.equal(true); - if (result.success) { - const expectedReturnBlock = `return { + expect(result.success).to.equal(true); + if (result.success) { + const expectedReturnBlock = `return { name: faker.person.fullName(), email: faker.internet.email() };`; - expect(result.script).to.contain(expectedReturnBlock); - expect(result.script).to.contain("use('testdb')"); - expect(result.script).to.contain('insertMany'); - } - }); + expect(result.script).to.contain(expectedReturnBlock); + expect(result.script).to.contain("use('testdb')"); + expect(result.script).to.contain('insertMany'); + } }); - describe('Array generation', () => { - it('should generate script for simple arrays', () => { - const schema = { - 'tags[]': createFieldMapping('lorem.word'), - }; + it('should generate script for simple arrays', () => { + const schema = { + 'tags[]': createFieldMapping('lorem.word'), + }; - const result = generateScript(schema, { - databaseName: 'testdb', - collectionName: 'posts', - documentCount: 3, - }); + const result = generateScript(schema, { + databaseName: 'testdb', + collectionName: 'posts', + documentCount: 3, + }); - expect(result.success).to.equal(true); - if (result.success) { - const expectedReturnBlock = `return { + 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(expectedReturnBlock); + } + }); - it('should generate script for arrays of objects', () => { - const schema = { - 'users[].name': createFieldMapping('person.fullName'), - 'users[].email': createFieldMapping('internet.email'), - }; + it('should generate script for arrays of objects', () => { + const schema = { + 'users[].name': createFieldMapping('person.fullName'), + 'users[].email': createFieldMapping('internet.email'), + }; - const result = generateScript(schema, { - databaseName: 'testdb', - collectionName: 'teams', - documentCount: 2, - }); + const result = generateScript(schema, { + databaseName: 'testdb', + collectionName: 'teams', + documentCount: 2, + }); - expect(result.success).to.equal(true); - if (result.success) { - // Should generate the complete return block with proper structure - const expectedReturnBlock = `return { + 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}, () => { name: faker.person.fullName(), email: faker.internet.email() }) };`; - expect(result.script).to.contain(expectedReturnBlock); - } - }); + expect(result.script).to.contain(expectedReturnBlock); + } + }); - it('should generate script for multi-dimensional arrays', () => { - const schema = { - 'matrix[][]': createFieldMapping('number.int'), - }; + it('should generate script for multi-dimensional arrays', () => { + const schema = { + 'matrix[][]': createFieldMapping('number.int'), + }; - const result = generateScript(schema, { - databaseName: 'testdb', - collectionName: 'data', - documentCount: 1, - }); + const result = generateScript(schema, { + databaseName: 'testdb', + collectionName: 'data', + documentCount: 1, + }); - expect(result.success).to.equal(true); - if (result.success) { - const expectedReturnBlock = `return { + expect(result.success).to.equal(true); + if (result.success) { + const expectedReturnBlock = `return { matrix: Array.from({length: 3}, () => Array.from({length: 3}, () => faker.number.int())) };`; - expect(result.script).to.contain(expectedReturnBlock); - } - }); + expect(result.script).to.contain(expectedReturnBlock); + } + }); - it('should generate script for arrays of objects with arrays', () => { - const schema = { - 'users[].name': createFieldMapping('person.fullName'), - 'users[].tags[]': createFieldMapping('lorem.word'), - }; + it('should generate script for arrays of objects with arrays', () => { + const schema = { + 'users[].name': createFieldMapping('person.fullName'), + 'users[].tags[]': createFieldMapping('lorem.word'), + }; - const result = generateScript(schema, { - databaseName: 'testdb', - collectionName: 'profiles', - documentCount: 1, - }); + const result = generateScript(schema, { + databaseName: 'testdb', + collectionName: 'profiles', + documentCount: 1, + }); - expect(result.success).to.equal(true); - if (result.success) { - const expectedReturnBlock = `return { + expect(result.success).to.equal(true); + if (result.success) { + const expectedReturnBlock = `return { users: Array.from({length: 3}, () => { name: faker.person.fullName(), tags: Array.from({length: 3}, () => faker.lorem.word()) }) };`; - expect(result.script).to.contain(expectedReturnBlock); - } - }); + expect(result.script).to.contain(expectedReturnBlock); + } }); - describe('Complex nested structures', () => { - it('should handle deeply nested object paths', () => { - const schema = { - 'users[].profile.address.street': createFieldMapping( - 'location.streetAddress' - ), - 'users[].profile.address.city': createFieldMapping('location.city'), - }; - - const result = generateScript(schema, { - databaseName: 'testdb', - collectionName: 'accounts', - documentCount: 1, - }); + it('should handle mixed field types and complex documents', () => { + const schema = { + title: createFieldMapping('lorem.sentence'), + 'authors[].name': createFieldMapping('person.fullName'), + 'authors[].books[]': createFieldMapping('lorem.words'), + publishedYear: createFieldMapping('date.recent'), + }; - expect(result.success).to.equal(true); - if (result.success) { - const expectedReturnBlock = `return { - users: Array.from({length: 3}, () => { - profile: { - address: { - street: faker.location.streetAddress(), - city: faker.location.city() - } - } - }) - };`; - expect(result.script).to.contain(expectedReturnBlock); - } + const result = generateScript(schema, { + databaseName: 'library', + collectionName: 'publications', + documentCount: 2, }); - it('should handle mixed field types', () => { - const schema = { - title: createFieldMapping('lorem.sentence'), - 'authors[].name': createFieldMapping('person.fullName'), - 'authors[].books[]': createFieldMapping('lorem.words'), - publishedYear: createFieldMapping('date.recent'), - }; - - const result = generateScript(schema, { - databaseName: 'library', - collectionName: 'publications', - documentCount: 2, - }); - - expect(result.success).to.equal(true); - if (result.success) { - const expectedReturnBlock = `return { + expect(result.success).to.equal(true); + if (result.success) { + const expectedReturnBlock = `return { title: faker.lorem.sentence(), authors: Array.from({length: 3}, () => { name: faker.person.fullName(), @@ -184,9 +149,8 @@ describe('Script Generation', () => { }), publishedYear: faker.date.recent() };`; - expect(result.script).to.contain(expectedReturnBlock); - } - }); + expect(result.script).to.contain(expectedReturnBlock); + } }); describe('Edge cases', () => { @@ -226,6 +190,58 @@ describe('Script Generation', () => { expect(result.script).to.contain(expectedReturnBlock); } }); + + it('should handle field names with brackets (non-array)', () => { + const schema = { + 'settings[theme]': createFieldMapping('lorem.word'), + 'data[0]': createFieldMapping('lorem.word'), + 'bracket]field': createFieldMapping('lorem.word'), + '[metadata': createFieldMapping('lorem.word'), + }; + + const result = generateScript(schema, { + databaseName: 'test', + collectionName: 'test', + documentCount: 1, + }); + + expect(result.success).to.equal(true); + if (result.success) { + // All fields should be treated as regular field names, not arrays + expect(result.script).to.contain('settings[theme]: faker.lorem.word()'); + expect(result.script).to.contain('data[0]: faker.lorem.word()'); + expect(result.script).to.contain('bracket]field: faker.lorem.word()'); + expect(result.script).to.contain('[metadata: faker.lorem.word()'); + expect(result.script).not.to.contain('Array.from'); + } + }); + + it('should handle field names with [] in middle (not array notation)', () => { + const schema = { + 'squareBrackets[]InMiddle': createFieldMapping('lorem.word'), + 'field[]WithMore': createFieldMapping('lorem.word'), + 'start[]middle[]end': createFieldMapping('lorem.word'), + }; + + const result = generateScript(schema, { + databaseName: 'test', + collectionName: 'test', + documentCount: 1, + }); + + expect(result.success).to.equal(true); + if (result.success) { + // These should be treated as regular field names, not arrays + expect(result.script).to.contain( + 'squareBrackets[]InMiddle: faker.lorem.word()' + ); + expect(result.script).to.contain('field[]WithMore: faker.lorem.word()'); + expect(result.script).to.contain( + 'start[]middle[]end: faker.lorem.word()' + ); + expect(result.script).not.to.contain('Array.from'); + } + }); }); describe('Configurable Array Lengths', () => { @@ -277,15 +293,22 @@ describe('Script Generation', () => { documentCount: 1, arrayLengthMap: { users: { - tags: [4], + length: 5, + elements: { + tags: [4], + }, }, }, }); expect(result.success).to.equal(true); if (result.success) { - // Should have tags array with length 4 - expect(result.script).to.contain('Array.from({length: 4}'); + const expectedReturnBlock = `return { + users: Array.from({length: 5}, () => { + tags: Array.from({length: 4}, () => faker.lorem.word()) + }) + };`; + expect(result.script).to.contain(expectedReturnBlock); } }); @@ -342,6 +365,53 @@ describe('Script Generation', () => { expect(result.script).to.contain(expectedReturnBlock); } }); + + it('should handle complex nested array configurations', () => { + const schema = { + 'users[].name': createFieldMapping('person.fullName'), + 'users[].tags[]': createFieldMapping('lorem.word'), + 'users[].posts[].title': createFieldMapping('lorem.sentence'), + 'users[].posts[].comments[]': createFieldMapping('lorem.words'), + 'matrix[][]': createFieldMapping('number.int'), + }; + + const result = generateScript(schema, { + databaseName: 'testdb', + collectionName: 'complex', + documentCount: 1, + arrayLengthMap: { + users: { + length: 2, + elements: { + tags: [3], + posts: { + length: 4, + elements: { + comments: [5], + }, + }, + }, + }, + matrix: [2, 3], + }, + }); + + expect(result.success).to.equal(true); + if (result.success) { + // Users array should have 2 elements + expect(result.script).to.contain('users: Array.from({length: 2}'); + // Each user's tags should have 3 elements + expect(result.script).to.contain('tags: Array.from({length: 3}'); + // Each user's posts should have 4 elements + expect(result.script).to.contain('posts: Array.from({length: 4}'); + // Each post's comments should have 5 elements + expect(result.script).to.contain('comments: Array.from({length: 5}'); + // Matrix should be 2x3 + expect(result.script).to.contain( + 'matrix: Array.from({length: 2}, () => Array.from({length: 3}' + ); + } + }); }); describe('Unrecognized Field Defaults', () => { @@ -706,28 +776,47 @@ describe('Script Generation', () => { } }); - it('should use probabilistic rendering when probability < 1.0', () => { + it('should default invalid probability to 1.0', () => { const schema = { - optionalField: createFieldMapping('lorem.word', 0.7), + field1: { + mongoType: 'string', + fakerMethod: 'lorem.word', + fakerArgs: [], + probability: 1.5, // Invalid - should default to 1.0 + }, + field2: { + mongoType: 'string', + fakerMethod: 'lorem.word', + fakerArgs: [], + probability: -0.5, // Invalid - should default to 1.0 + }, + field3: { + mongoType: 'string', + fakerMethod: 'lorem.word', + fakerArgs: [], + probability: 'invalid' as any, // Invalid - should default to 1.0 + }, }; const result = generateScript(schema, { - databaseName: 'testdb', - collectionName: 'posts', + databaseName: 'test', + collectionName: 'test', documentCount: 1, }); expect(result.success).to.equal(true); if (result.success) { - expect(result.script).to.contain( - '...(Math.random() < 0.7 ? { optionalField: faker.lorem.word() } : {})' - ); + // All fields should be treated as probability 1.0 (always present) + expect(result.script).to.contain('field1: faker.lorem.word()'); + expect(result.script).to.contain('field2: faker.lorem.word()'); + expect(result.script).to.contain('field3: faker.lorem.word()'); + expect(result.script).not.to.contain('Math.random()'); } }); - it('should handle zero probability', () => { + it('should use probabilistic rendering when probability < 1.0', () => { const schema = { - rareField: createFieldMapping('lorem.word', 0), + optionalField: createFieldMapping('lorem.word', 0.7), }; const result = generateScript(schema, { @@ -739,7 +828,7 @@ describe('Script Generation', () => { expect(result.success).to.equal(true); if (result.success) { expect(result.script).to.contain( - '...(Math.random() < 0 ? { rareField: faker.lorem.word() } : {})' + '...(Math.random() < 0.7 ? { optionalField: faker.lorem.word() } : {})' ); } }); @@ -832,117 +921,4 @@ describe('Script Generation', () => { } }); }); - - describe('Graceful Handling', () => { - it('should default invalid probability to 1.0', () => { - const schema = { - field1: { - mongoType: 'string', - fakerMethod: 'lorem.word', - fakerArgs: [], - probability: 1.5, // Invalid - should default to 1.0 - }, - field2: { - mongoType: 'string', - fakerMethod: 'lorem.word', - fakerArgs: [], - probability: -0.5, // Invalid - should default to 1.0 - }, - field3: { - mongoType: 'string', - fakerMethod: 'lorem.word', - fakerArgs: [], - probability: 'invalid' as any, // Invalid - should default to 1.0 - }, - }; - - const result = generateScript(schema, { - databaseName: 'test', - collectionName: 'test', - documentCount: 1, - }); - - expect(result.success).to.equal(true); - if (result.success) { - // All fields should be treated as probability 1.0 (always present) - expect(result.script).to.contain('field1: faker.lorem.word()'); - expect(result.script).to.contain('field2: faker.lorem.word()'); - expect(result.script).to.contain('field3: faker.lorem.word()'); - expect(result.script).not.to.contain('Math.random()'); - } - }); - - it('should handle field names with brackets (non-array)', () => { - const schema = { - 'settings[theme]': createFieldMapping('lorem.word'), - 'data[0]': createFieldMapping('lorem.word'), - 'bracket]field': createFieldMapping('lorem.word'), - '[metadata': createFieldMapping('lorem.word'), - }; - - const result = generateScript(schema, { - databaseName: 'test', - collectionName: 'test', - documentCount: 1, - }); - - expect(result.success).to.equal(true); - if (result.success) { - // All fields should be treated as regular field names, not arrays - expect(result.script).to.contain('settings[theme]: faker.lorem.word()'); - expect(result.script).to.contain('data[0]: faker.lorem.word()'); - expect(result.script).to.contain('bracket]field: faker.lorem.word()'); - expect(result.script).to.contain('[metadata: faker.lorem.word()'); - expect(result.script).not.to.contain('Array.from'); - } - }); - - it('should handle field names with [] in middle (not array notation)', () => { - const schema = { - 'squareBrackets[]InMiddle': createFieldMapping('lorem.word'), - 'field[]WithMore': createFieldMapping('lorem.word'), - 'start[]middle[]end': createFieldMapping('lorem.word'), - }; - - const result = generateScript(schema, { - databaseName: 'test', - collectionName: 'test', - documentCount: 1, - }); - - expect(result.success).to.equal(true); - if (result.success) { - // These should be treated as regular field names, not arrays - expect(result.script).to.contain( - 'squareBrackets[]InMiddle: faker.lorem.word()' - ); - expect(result.script).to.contain('field[]WithMore: faker.lorem.word()'); - expect(result.script).to.contain( - 'start[]middle[]end: faker.lorem.word()' - ); - expect(result.script).not.to.contain('Array.from'); - } - }); - - it('should still handle real array notation correctly', () => { - const schema = { - 'realArray[]': createFieldMapping('lorem.word'), - 'nestedArray[].field': createFieldMapping('lorem.word'), - }; - - const result = generateScript(schema, { - databaseName: 'test', - collectionName: 'test', - documentCount: 1, - }); - - expect(result.success).to.equal(true); - if (result.success) { - // These should be treated as arrays - expect(result.script).to.contain('Array.from'); - expect(result.script).to.contain('realArray: Array.from'); - expect(result.script).to.contain('nestedArray: Array.from'); - } - }); - }); }); 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 31639339358..674bb91071d 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 @@ -10,11 +10,18 @@ export interface FieldMapping { probability?: number; // 0.0 - 1.0 frequency of field (defaults to 1.0) } -// Hierarchical array length map that mirrors document structure +// Array length configuration for different array types export type ArrayLengthMap = { - [fieldName: string]: number[] | 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 interface ScriptOptions { documentCount: number; databaseName: string; @@ -362,11 +369,12 @@ function generateDocumentCode( } else { // It's a nested object: recursive call - // Type Validation/ Fallback for misconfigured array length map - const arrayDimensions = arrayLengthMap[fieldName]; + // Get nested array length map for this field, + // including type validation and fallback for malformed maps + const arrayInfo = arrayLengthMap[fieldName]; const nestedArrayLengthMap = - typeof arrayDimensions === 'object' && !Array.isArray(arrayDimensions) - ? arrayDimensions + arrayInfo && !Array.isArray(arrayInfo) && 'elements' in arrayInfo + ? arrayInfo.elements : {}; const nestedCode = generateDocumentCode( @@ -399,10 +407,16 @@ function generateArrayCode( const elementType = arrayStructure.elementType; // Get array length for this dimension - const arrayDimensions = arrayLengthMap[fieldName]; - const arrayLength = Array.isArray(arrayDimensions) - ? arrayDimensions[dimensionIndex] ?? DEFAULT_ARRAY_LENGTH - : DEFAULT_ARRAY_LENGTH; // Fallback for misconfigured array length map + 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 ('mongoType' in elementType) { // Array of primitives @@ -421,9 +435,9 @@ function generateArrayCode( } else { // Array of objects const nestedArrayLengthMap = - typeof arrayDimensions === 'object' && !Array.isArray(arrayDimensions) - ? arrayDimensions - : {}; // Fallback for misconfigured array length map + arrayInfo && !Array.isArray(arrayInfo) && 'elements' in arrayInfo + ? arrayInfo.elements + : {}; // Fallback to empty map for malformed array map const objectCode = generateDocumentCode( elementType as DocumentStructure, indent, From b84b769da06f817e5ccbf95a68738593fba977ec Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Fri, 12 Sep 2025 16:01:23 -0400 Subject: [PATCH 14/31] Address comments: move function, remove fallback --- .../script-generation-utils.ts | 125 +++++++++--------- 1 file changed, 63 insertions(+), 62 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 674bb91071d..8591f54a2a7 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 @@ -47,6 +47,68 @@ interface ArrayStructure { elementType: FieldMapping | DocumentStructure | ArrayStructure; } +/** + * Entry point method: Generate the final script + */ +export function generateScript( + schema: Record, + options: ScriptOptions +): ScriptResult { + try { + const structure = buildDocumentStructure(schema); + + const documentCode = generateDocumentCode( + structure, + INDENT_SIZE * 2, // 4 spaces: 2 for function body + 2 for inside return statement + options.arrayLengthMap + ); + + // Escape ' and ` in database/collection names for template literals + const escapedDbName = options.databaseName + .replace(/'/g, "\\'") + .replace(/`/g, '\\`'); + const escapedCollectionName = options.collectionName + .replace(/'/g, "\\'") + .replace(/`/g, '\\`'); + + const script = `// Mock Data Generator Script +// Generated for collection: ${escapedDbName}.${escapedCollectionName} +// Document count: ${options.documentCount} + +const { faker } = require('@faker-js/faker'); + +// Connect to database +use('${escapedDbName}'); + +// Document generation function +function generateDocument() { + return ${documentCode}; +} + +// Generate and insert documents +const documents = []; +for (let i = 0; i < ${options.documentCount}; i++) { + documents.push(generateDocument()); +} + +// Insert documents into collection +db.getCollection('${escapedCollectionName}').insertMany(documents); + +console.log(\`Successfully inserted \${documents.length} documents into ${escapedDbName}.${escapedCollectionName}\`);`; + + return { + script, + success: true, + }; + } catch (error) { + return { + script: '', + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + }; + } +} + /** * Parse a field path into simple parts * @@ -113,6 +175,7 @@ function parseFieldPath(fieldPath: string): string[] { return parts; } + /** * Build the document structure from all field paths */ @@ -252,68 +315,6 @@ function insertIntoStructure( } } -/** - * Generate the final script - */ -export function generateScript( - schema: Record, - options: ScriptOptions -): ScriptResult { - try { - const structure = buildDocumentStructure(schema); - - const documentCode = generateDocumentCode( - structure, - INDENT_SIZE * 2, // 4 spaces: 2 for function body + 2 for inside return statement - options.arrayLengthMap || {} - ); - - // Escape ' and ` in database/collection names for template literals - const escapedDbName = options.databaseName - .replace(/'/g, "\\'") - .replace(/`/g, '\\`'); - const escapedCollectionName = options.collectionName - .replace(/'/g, "\\'") - .replace(/`/g, '\\`'); - - const script = `// Mock Data Generator Script -// Generated for collection: ${escapedDbName}.${escapedCollectionName} -// Document count: ${options.documentCount} - -const { faker } = require('@faker-js/faker'); - -// Connect to database -use('${escapedDbName}'); - -// Document generation function -function generateDocument() { - return ${documentCode}; -} - -// Generate and insert documents -const documents = []; -for (let i = 0; i < ${options.documentCount}; i++) { - documents.push(generateDocument()); -} - -// Insert documents into collection -db.getCollection('${escapedCollectionName}').insertMany(documents); - -console.log(\`Successfully inserted \${documents.length} documents into ${escapedDbName}.${escapedCollectionName}\`);`; - - return { - script, - success: true, - }; - } catch (error) { - return { - script: '', - success: false, - error: error instanceof Error ? error.message : 'Unknown error', - }; - } -} - /** * Generate JavaScript object code from document structure */ From 43c70dbbfa7f2afc62ce698b0e8b84ed1cb31126 Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Fri, 12 Sep 2025 16:03:52 -0400 Subject: [PATCH 15/31] Tweak type --- .../mock-data-generator-modal/script-generation-utils.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 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 8591f54a2a7..fde1ea5c83f 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 @@ -29,11 +29,9 @@ export interface ScriptOptions { arrayLengthMap?: ArrayLengthMap; } -export interface ScriptResult { - script: string; - success: boolean; - error?: string; -} +export type ScriptResult = + | { script: string; success: true } + | { error: string; success: false }; type DocumentStructure = { [fieldName: string]: @@ -102,7 +100,6 @@ console.log(\`Successfully inserted \${documents.length} documents into ${escape }; } catch (error) { return { - script: '', success: false, error: error instanceof Error ? error.message : 'Unknown error', }; From 1847b057b27c62768b21f70c26c6160e08dd5be7 Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Fri, 12 Sep 2025 16:04:54 -0400 Subject: [PATCH 16/31] Rename methods --- .../script-generation-utils.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 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 fde1ea5c83f..2626815f545 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 @@ -55,7 +55,7 @@ export function generateScript( try { const structure = buildDocumentStructure(schema); - const documentCode = generateDocumentCode( + const documentCode = renderDocumentCode( structure, INDENT_SIZE * 2, // 4 spaces: 2 for function body + 2 for inside return statement options.arrayLengthMap @@ -315,7 +315,7 @@ function insertIntoStructure( /** * Generate JavaScript object code from document structure */ -function generateDocumentCode( +function renderDocumentCode( structure: DocumentStructure, indent: number = INDENT_SIZE, arrayLengthMap: ArrayLengthMap = {} @@ -356,7 +356,7 @@ function generateDocumentCode( } } else if ('type' in value && value.type === 'array') { // It's an array - const arrayCode = generateArrayCode( + const arrayCode = renderArrayCode( value as ArrayStructure, indent + INDENT_SIZE, fieldName, @@ -375,7 +375,7 @@ function generateDocumentCode( ? arrayInfo.elements : {}; - const nestedCode = generateDocumentCode( + const nestedCode = renderDocumentCode( value as DocumentStructure, indent + INDENT_SIZE, nestedArrayLengthMap @@ -395,7 +395,7 @@ function generateDocumentCode( /** * Generate array code */ -function generateArrayCode( +function renderArrayCode( arrayStructure: ArrayStructure, indent: number = INDENT_SIZE, fieldName: string = '', @@ -422,7 +422,7 @@ function generateArrayCode( return `Array.from({length: ${arrayLength}}, () => ${fakerCall})`; } else if ('type' in elementType && elementType.type === 'array') { // Nested array (e.g., matrix[][]) - keep same fieldName, increment dimension - const nestedArrayCode = generateArrayCode( + const nestedArrayCode = renderArrayCode( elementType as ArrayStructure, indent, fieldName, @@ -436,7 +436,7 @@ function generateArrayCode( arrayInfo && !Array.isArray(arrayInfo) && 'elements' in arrayInfo ? arrayInfo.elements : {}; // Fallback to empty map for malformed array map - const objectCode = generateDocumentCode( + const objectCode = renderDocumentCode( elementType as DocumentStructure, indent, nestedArrayLengthMap From d5056c0cad3da9a5979baab2758bcbc0a875a144 Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Fri, 12 Sep 2025 16:11:50 -0400 Subject: [PATCH 17/31] Rename variable --- .../script-generation-utils.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 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 2626815f545..1904f569fa7 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 @@ -327,7 +327,7 @@ function renderDocumentCode( const fieldIndent = ' '.repeat(indent); const closingBraceIndent = ' '.repeat(indent - INDENT_SIZE); - const rootLevelFields: string[] = []; + const documentFields: string[] = []; for (const [fieldName, value] of Object.entries(structure)) { if ('mongoType' in value) { @@ -347,12 +347,12 @@ function renderDocumentCode( if (probability < 1.0) { // Use Math.random for conditional field inclusion - rootLevelFields.push( + documentFields.push( `${fieldIndent}...(Math.random() < ${probability} ? { ${fieldName}: ${fakerCall} } : {})` ); } else { // Normal field inclusion - rootLevelFields.push(`${fieldIndent}${fieldName}: ${fakerCall}`); + documentFields.push(`${fieldIndent}${fieldName}: ${fakerCall}`); } } else if ('type' in value && value.type === 'array') { // It's an array @@ -363,7 +363,7 @@ function renderDocumentCode( arrayLengthMap, 0 // Start at dimension 0 ); - rootLevelFields.push(`${fieldIndent}${fieldName}: ${arrayCode}`); + documentFields.push(`${fieldIndent}${fieldName}: ${arrayCode}`); } else { // It's a nested object: recursive call @@ -380,16 +380,16 @@ function renderDocumentCode( indent + INDENT_SIZE, nestedArrayLengthMap ); - rootLevelFields.push(`${fieldIndent}${fieldName}: ${nestedCode}`); + documentFields.push(`${fieldIndent}${fieldName}: ${nestedCode}`); } } // Handle empty objects - if (rootLevelFields.length === 0) { + if (documentFields.length === 0) { return '{}'; } - return `{\n${rootLevelFields.join(',\n')}\n${closingBraceIndent}}`; + return `{\n${documentFields.join(',\n')}\n${closingBraceIndent}}`; } /** From 578a6328f22f24cd02aa73c0791107d9af584c2a Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Fri, 12 Sep 2025 16:16:22 -0400 Subject: [PATCH 18/31] use JSON.stringify to escape characters --- .../script-generation-utils.spec.ts | 26 ++++++++++++++++++- .../script-generation-utils.ts | 20 ++++++-------- 2 files changed, 33 insertions(+), 13 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 72564009f7d..9d820e62541 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 @@ -31,7 +31,7 @@ describe('Script Generation', () => { email: faker.internet.email() };`; expect(result.script).to.contain(expectedReturnBlock); - expect(result.script).to.contain("use('testdb')"); + expect(result.script).to.contain('use("testdb")'); expect(result.script).to.contain('insertMany'); } }); @@ -242,6 +242,30 @@ describe('Script Generation', () => { expect(result.script).not.to.contain('Array.from'); } }); + + it('should safely handle special characters in database and collection names', () => { + const schema = { + name: createFieldMapping('person.fullName'), + }; + + const result = generateScript(schema, { + databaseName: 'test\'db`with"quotes', + collectionName: 'coll\nwith\ttabs', + 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")'); + expect(result.script).to.contain( + 'db.getCollection("coll\\nwith\\ttabs")' + ); + // 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"); + } + }); }); describe('Configurable Array Lengths', () => { 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 1904f569fa7..d676a739d87 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 @@ -61,22 +61,14 @@ export function generateScript( options.arrayLengthMap ); - // Escape ' and ` in database/collection names for template literals - const escapedDbName = options.databaseName - .replace(/'/g, "\\'") - .replace(/`/g, '\\`'); - const escapedCollectionName = options.collectionName - .replace(/'/g, "\\'") - .replace(/`/g, '\\`'); - const script = `// Mock Data Generator Script -// Generated for collection: ${escapedDbName}.${escapedCollectionName} +// Generated for collection: ${options.databaseName}.${options.collectionName} // Document count: ${options.documentCount} const { faker } = require('@faker-js/faker'); // Connect to database -use('${escapedDbName}'); +use(${JSON.stringify(options.databaseName)}); // Document generation function function generateDocument() { @@ -90,9 +82,13 @@ for (let i = 0; i < ${options.documentCount}; i++) { } // Insert documents into collection -db.getCollection('${escapedCollectionName}').insertMany(documents); +db.getCollection(${JSON.stringify( + options.collectionName + )}).insertMany(documents); -console.log(\`Successfully inserted \${documents.length} documents into ${escapedDbName}.${escapedCollectionName}\`);`; +console.log(\`Successfully inserted \${documents.length} documents into ${ + options.databaseName + }.${options.collectionName}\`);`; return { script, From df8ead5690e988fb803049bd0cbf517895898454 Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Fri, 12 Sep 2025 16:22:58 -0400 Subject: [PATCH 19/31] Json.Stringify --- .../script-generation-utils.spec.ts | 34 +++++++++++++++++-- .../script-generation-utils.ts | 4 +-- 2 files changed, 32 insertions(+), 6 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 9d820e62541..8b1c7645708 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 @@ -605,7 +605,7 @@ describe('Script Generation', () => { expect(result.success).to.equal(true); if (result.success) { - expect(result.script).to.contain("faker.person.firstName('male')"); + expect(result.script).to.contain('faker.person.firstName("male")'); } }); @@ -718,7 +718,7 @@ describe('Script Generation', () => { } }); - it('should escape quotes in string arguments', () => { + it('should safely handle quotes and special characters in string arguments', () => { const schema = { quote: { mongoType: 'string', @@ -736,7 +736,7 @@ describe('Script Generation', () => { expect(result.success).to.equal(true); if (result.success) { expect(result.script).to.contain( - "faker.lorem.sentence('It\\'s a \\'test\\' string')" + "faker.lorem.sentence(\"It's a 'test' string\")" ); } }); @@ -761,6 +761,34 @@ describe('Script Generation', () => { expect(result.script).to.contain('faker.string.uuid()'); } }); + + it('should safely handle complex string arguments with newlines and special characters', () => { + const schema = { + complexString: { + mongoType: 'string', + fakerMethod: 'lorem.sentence', + fakerArgs: [ + 'Line 1\nLine 2\tTabbed\r\nWith "quotes" and \'apostrophes\'', + ], + }, + }; + + const result = generateScript(schema, { + databaseName: 'testdb', + collectionName: 'test', + documentCount: 1, + }); + + expect(result.success).to.equal(true); + if (result.success) { + // Should use JSON.stringify for safe string serialization + expect(result.script).to.contain( + 'faker.lorem.sentence("Line 1\\nLine 2\\tTabbed\\r\\nWith \\"quotes\\" and \'apostrophes\'")' + ); + // Should not contain unescaped newlines that would break JS + expect(result.script).not.to.contain('Line 1\nLine 2'); + } + }); }); describe('Probability Handling', () => { 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 d676a739d87..51c0b2be436 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 @@ -491,9 +491,7 @@ export function formatFakerArgs(fakerArgs: FakerArg[]): string { const arg = fakerArgs[i]; if (typeof arg === 'string') { - // Escape single quotes for JS strings (and backticks for security) - const escapedArg = arg.replace(/[`']/g, '\\$&'); - stringifiedArgs.push(`'${escapedArg}'`); + stringifiedArgs.push(JSON.stringify(arg)); } else if (typeof arg === 'number') { if (!Number.isFinite(arg)) { throw new Error( From ca42248b8248d79e5ef317259b2215912d230a2f Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Fri, 12 Sep 2025 16:28:12 -0400 Subject: [PATCH 20/31] Make javadoc more descriptive --- .../script-generation-utils.ts | 16 +++++++++++++++- 1 file changed, 15 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 51c0b2be436..ef051607fca 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 @@ -482,7 +482,21 @@ export function getDefaultFakerMethod(mongoType: string): string { } /** - * Converts faker arguments to JavaScript code + * Converts array of faker arguments to comma separated string for function calls. + * + * Serializes various argument types into valid JavaScript syntax: + * - Strings: Uses JSON.stringify() to handle quotes, newlines, and special characters + * - Numbers: Validates finite numbers and converts to string representation + * - Booleans: Converts to 'true' or 'false' literals + * - Objects with 'json' property: Parses and re-stringifies JSON for validation + * + * @param fakerArgs - Array of arguments to convert to JavaScript code + * @returns Comma-separated string of JavaScript arguments, or empty string if no args + * @throws Error if arguments contain invalid values (non-finite numbers, malformed JSON) + * + * @example + * formatFakerArgs(['male', 25, true]) // Returns: '"male", 25, true' + * formatFakerArgs([{json: '{"min": 1}'}]) // Returns: '{"min": 1}' */ export function formatFakerArgs(fakerArgs: FakerArg[]): string { const stringifiedArgs: string[] = []; From dca8dc0a3420abc9bc5f8e4bcab140e08f321dc7 Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Fri, 12 Sep 2025 16:56:00 -0400 Subject: [PATCH 21/31] Fix object --- .../mock-data-generator-modal/script-generation-utils.ts | 2 +- 1 file changed, 1 insertion(+), 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 ef051607fca..548a4119b2d 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 @@ -437,7 +437,7 @@ function renderArrayCode( indent, nestedArrayLengthMap ); - return `Array.from({length: ${arrayLength}}, () => ${objectCode})`; + return `Array.from({length: ${arrayLength}}, () => (${objectCode}))`; } } From 5ede61a9a258eb9f4e457e564d8895932a968e4a Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Fri, 12 Sep 2025 17:01:24 -0400 Subject: [PATCH 22/31] Tests --- .../script-generation-utils.spec.ts | 121 ++++++++++++++++-- 1 file changed, 108 insertions(+), 13 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 8b1c7645708..5eafd99578a 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,32 @@ import { expect } from 'chai'; +import { faker } from '@faker-js/faker/locale/en'; import { generateScript, type FieldMapping } from './script-generation-utils'; +/** + * Helper function to test that generated document code is executable + */ +function testDocumentCodeExecution(script: string): any { + // Extract the return statement from the generateDocument function + const returnMatch = script.match(/return ([\s\S]*?);[\s]*\}/); + expect(returnMatch, 'Should contain return statement').to.not.be.null; + + const returnExpression = returnMatch![1]; + + try { + // Create a function that executes the document code with faker + // eslint-disable-next-line @typescript-eslint/no-implied-eval + const generateDocument = new Function( + 'faker', + `return ${returnExpression};` + ); + + // Execute and return the generated document + return generateDocument(faker); + } catch (error) { + throw new Error(`Failed to execute generated code: ${error.message}`); + } +} + describe('Script Generation', () => { const createFieldMapping = ( fakerMethod: string, @@ -33,6 +59,14 @@ describe('Script Generation', () => { expect(result.script).to.contain(expectedReturnBlock); expect(result.script).to.contain('use("testdb")'); expect(result.script).to.contain('insertMany'); + + // Test that the generated document code is executable + const document = testDocumentCodeExecution(result.script); + 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('email'); + expect(document.email).to.be.a('string').and.include('@'); } }); @@ -53,6 +87,13 @@ describe('Script Generation', () => { tags: Array.from({length: 3}, () => faker.lorem.word()) };`; expect(result.script).to.contain(expectedReturnBlock); + + // Test that the generated document code is executable + const document = testDocumentCodeExecution(result.script); + expect(document).to.be.an('object'); + expect(document).to.have.property('tags'); + expect(document.tags).to.be.an('array').with.length(3); + expect(document.tags[0]).to.be.a('string').and.not.be.empty; } }); @@ -71,13 +112,19 @@ 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}, () => { - name: faker.person.fullName(), - email: faker.internet.email() - }) - };`; - expect(result.script).to.contain(expectedReturnBlock); + expect(result.script).to.contain('Array.from({length: 3}'); + expect(result.script).to.contain('faker.person.fullName()'); + expect(result.script).to.contain('faker.internet.email()'); + + // Test that the generated document code is executable + const document = testDocumentCodeExecution(result.script); + expect(document).to.be.an('object'); + expect(document).to.have.property('users'); + expect(document.users).to.be.an('array').with.length(3); + expect(document.users[0]).to.have.property('name'); + expect(document.users[0].name).to.be.a('string').and.not.be.empty; + expect(document.users[0]).to.have.property('email'); + expect(document.users[0].email).to.be.a('string').and.include('@'); } }); @@ -98,6 +145,14 @@ describe('Script Generation', () => { matrix: Array.from({length: 3}, () => Array.from({length: 3}, () => faker.number.int())) };`; expect(result.script).to.contain(expectedReturnBlock); + + // Test that the generated document code is executable + const document = testDocumentCodeExecution(result.script); + expect(document).to.be.an('object'); + expect(document).to.have.property('matrix'); + expect(document.matrix).to.be.an('array').with.length(3); + expect(document.matrix[0]).to.be.an('array').with.length(3); + expect(document.matrix[0][0]).to.be.a('number'); } }); @@ -116,12 +171,23 @@ 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()) - }) + })) };`; expect(result.script).to.contain(expectedReturnBlock); + + // Test that the generated document code is executable + const document = testDocumentCodeExecution(result.script); + expect(document).to.be.an('object'); + expect(document).to.have.property('users'); + expect(document.users).to.be.an('array').with.length(3); + expect(document.users[0]).to.have.property('name'); + expect(document.users[0].name).to.be.a('string').and.not.be.empty; + expect(document.users[0]).to.have.property('tags'); + expect(document.users[0].tags).to.be.an('array').with.length(3); + expect(document.users[0].tags[0]).to.be.a('string').and.not.be.empty; } }); @@ -143,13 +209,27 @@ 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()) - }), + })), publishedYear: faker.date.recent() };`; expect(result.script).to.contain(expectedReturnBlock); + + // Test that the generated document code is executable + const document = testDocumentCodeExecution(result.script); + expect(document).to.be.an('object'); + expect(document).to.have.property('title'); + expect(document.title).to.be.a('string').and.not.be.empty; + expect(document).to.have.property('authors'); + expect(document.authors).to.be.an('array').with.length(3); + expect(document.authors[0]).to.have.property('name'); + expect(document.authors[0].name).to.be.a('string').and.not.be.empty; + expect(document.authors[0]).to.have.property('books'); + expect(document.authors[0].books).to.be.an('array').with.length(3); + expect(document).to.have.property('publishedYear'); + expect(document.publishedYear).to.be.a('date'); } }); @@ -188,6 +268,12 @@ describe('Script Generation', () => { value: faker.number.int() };`; expect(result.script).to.contain(expectedReturnBlock); + + // Test that the generated document code is executable + const document = testDocumentCodeExecution(result.script); + expect(document).to.be.an('object'); + expect(document).to.have.property('value'); + expect(document.value).to.be.a('number'); } }); @@ -328,11 +414,20 @@ describe('Script Generation', () => { expect(result.success).to.equal(true); if (result.success) { const expectedReturnBlock = `return { - users: Array.from({length: 5}, () => { + users: Array.from({length: 5}, () => ({ tags: Array.from({length: 4}, () => faker.lorem.word()) - }) + })) };`; expect(result.script).to.contain(expectedReturnBlock); + + // Test that the generated document code is executable + const document = testDocumentCodeExecution(result.script); + expect(document).to.be.an('object'); + expect(document).to.have.property('users'); + expect(document.users).to.be.an('array').with.length(5); + expect(document.users[0]).to.have.property('tags'); + expect(document.users[0].tags).to.be.an('array').with.length(4); + expect(document.users[0].tags[0]).to.be.a('string').and.not.be.empty; } }); From a2c071e3ee8ab7803d5ae909c79aa0e933827cac Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Mon, 15 Sep 2025 12:34:29 -0400 Subject: [PATCH 23/31] Tests --- package-lock.json | 25 +- packages/compass-collection/package.json | 1 + .../script-generation-utils.spec.ts | 334 +++++++++++------- .../script-generation-utils.ts | 32 +- 4 files changed, 264 insertions(+), 128 deletions(-) diff --git a/package-lock.json b/package-lock.json index e805f3720c5..a18b8894ecb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6031,6 +6031,22 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@faker-js/faker": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.4.1.tgz", + "integrity": "sha512-XQ3cU+Q8Uqmrbf2e0cIC/QN43sTBSC8KF12u29Mb47tWrt2hAgBXSgpZMj4Ao8Uk0iJcU99QsOCaIL8934obCg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/fakerjs" + } + ], + "license": "MIT", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0", + "npm": ">=6.14.13" + } + }, "node_modules/@fastify/busboy": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", @@ -47719,6 +47735,7 @@ "version": "4.71.1", "license": "SSPL", "dependencies": { + "@faker-js/faker": "^8.4.1", "@mongodb-js/compass-app-registry": "^9.4.21", "@mongodb-js/compass-app-stores": "^7.58.1", "@mongodb-js/compass-components": "^1.50.1", @@ -51579,7 +51596,6 @@ "@mongodb-js/devtools-proxy-support": "^0.5.2", "@mongodb-js/eslint-config-compass": "^1.4.8", "@mongodb-js/mocha-config-compass": "^1.7.1", - "@mongodb-js/my-queries-storage": "^0.39.1", "@mongodb-js/prettier-config-compass": "^1.2.8", "@mongodb-js/testing-library-compass": "^1.3.11", "@mongodb-js/tsconfig-compass": "^1.2.10", @@ -57967,6 +57983,11 @@ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==" }, + "@faker-js/faker": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.4.1.tgz", + "integrity": "sha512-XQ3cU+Q8Uqmrbf2e0cIC/QN43sTBSC8KF12u29Mb47tWrt2hAgBXSgpZMj4Ao8Uk0iJcU99QsOCaIL8934obCg==" + }, "@fastify/busboy": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", @@ -61215,6 +61236,7 @@ "@mongodb-js/compass-collection": { "version": "file:packages/compass-collection", "requires": { + "@faker-js/faker": "^8.4.1", "@mongodb-js/compass-app-registry": "^9.4.21", "@mongodb-js/compass-app-stores": "^7.58.1", "@mongodb-js/compass-components": "^1.50.1", @@ -64216,7 +64238,6 @@ "@mongodb-js/devtools-proxy-support": "^0.5.2", "@mongodb-js/eslint-config-compass": "^1.4.8", "@mongodb-js/mocha-config-compass": "^1.7.1", - "@mongodb-js/my-queries-storage": "^0.39.1", "@mongodb-js/prettier-config-compass": "^1.2.8", "@mongodb-js/testing-library-compass": "^1.3.11", "@mongodb-js/tsconfig-compass": "^1.2.10", diff --git a/packages/compass-collection/package.json b/packages/compass-collection/package.json index 31bdfbcd877..76c8bf25be9 100644 --- a/packages/compass-collection/package.json +++ b/packages/compass-collection/package.json @@ -48,6 +48,7 @@ "reformat": "npm run eslint . -- --fix && npm run prettier -- --write ." }, "dependencies": { + "@faker-js/faker": "^8.4.1", "@mongodb-js/compass-app-registry": "^9.4.21", "@mongodb-js/compass-app-stores": "^7.58.1", "@mongodb-js/compass-components": "^1.50.1", 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 5eafd99578a..4d7a0e6991a 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 @@ -4,27 +4,37 @@ import { generateScript, type FieldMapping } from './script-generation-utils'; /** * Helper function to test that generated document code is executable + * + * This function takes a complete mongosh script and extracts just the document + * generation logic to test it in isolation with the real faker.js library. + * + * @param script - Complete mongosh script containing a generateDocument function + * @returns The generated document object (for any possible extra validation) */ function testDocumentCodeExecution(script: string): any { + // The script contains: "function generateDocument() { return { ... }; }" + // The "{ ... }" part is the document structure + // Extract the return statement from the generateDocument function const returnMatch = script.match(/return ([\s\S]*?);[\s]*\}/); expect(returnMatch, 'Should contain return statement').to.not.be.null; + // Get the document structure expression (everything between "return" and ";") + // Example: "{ name: faker.person.fullName(), tags: Array.from({length: 3}, () => faker.lorem.word()) }" const returnExpression = returnMatch![1]; - try { - // Create a function that executes the document code with faker - // eslint-disable-next-line @typescript-eslint/no-implied-eval - const generateDocument = new Function( - 'faker', - `return ${returnExpression};` - ); - - // Execute and return the generated document - return generateDocument(faker); - } catch (error) { - throw new Error(`Failed to execute generated code: ${error.message}`); - } + // Create a new function + // This is equivalent to: function(faker) { return ; } + // The 'faker' parameter will receive the real faker.js library when we pass it in on call + // eslint-disable-next-line @typescript-eslint/no-implied-eval + const generateDocument = new Function( + 'faker', // Parameter name for the faker library + `return ${returnExpression};` // Function body: return the document structure + ); + + // Execute the function with the real faker library + // This actually generates a document using faker methods and returns it + return generateDocument(faker); } describe('Script Generation', () => { @@ -112,9 +122,13 @@ describe('Script Generation', () => { expect(result.success).to.equal(true); if (result.success) { // Should generate the complete return block with proper structure - expect(result.script).to.contain('Array.from({length: 3}'); - expect(result.script).to.contain('faker.person.fullName()'); - expect(result.script).to.contain('faker.internet.email()'); + const expectedReturnBlock = `return { + users: Array.from({length: 3}, () => ({ + name: faker.person.fullName(), + email: faker.internet.email() + })) + };`; + expect(result.script).to.contain(expectedReturnBlock); // Test that the generated document code is executable const document = testDocumentCodeExecution(result.script); @@ -248,6 +262,9 @@ describe('Script Generation', () => { if (result.success) { const expectedReturnBlock = `return {};`; expect(result.script).to.contain(expectedReturnBlock); + + // Test that the generated document code is executable + testDocumentCodeExecution(result.script); } }); @@ -294,11 +311,22 @@ describe('Script Generation', () => { expect(result.success).to.equal(true); if (result.success) { // All fields should be treated as regular field names, not arrays - expect(result.script).to.contain('settings[theme]: faker.lorem.word()'); - expect(result.script).to.contain('data[0]: faker.lorem.word()'); - expect(result.script).to.contain('bracket]field: faker.lorem.word()'); - expect(result.script).to.contain('[metadata: faker.lorem.word()'); + const expectedReturnBlock = `return { + "settings[theme]": faker.lorem.word(), + "data[0]": faker.lorem.word(), + "bracket]field": faker.lorem.word(), + "[metadata": faker.lorem.word() + };`; + expect(result.script).to.contain(expectedReturnBlock); expect(result.script).not.to.contain('Array.from'); + + // Test that the generated document code is executable + const document = testDocumentCodeExecution(result.script); + expect(document).to.be.an('object'); + expect(document).to.have.property('settings[theme]'); + expect(document).to.have.property('data[0]'); + expect(document).to.have.property('bracket]field'); + expect(document).to.have.property('[metadata'); } }); @@ -318,14 +346,20 @@ describe('Script Generation', () => { expect(result.success).to.equal(true); if (result.success) { // These should be treated as regular field names, not arrays - expect(result.script).to.contain( - 'squareBrackets[]InMiddle: faker.lorem.word()' - ); - expect(result.script).to.contain('field[]WithMore: faker.lorem.word()'); - expect(result.script).to.contain( - 'start[]middle[]end: faker.lorem.word()' - ); + 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); expect(result.script).not.to.contain('Array.from'); + + // Test that the generated document code is executable + const document = testDocumentCodeExecution(result.script); + expect(document).to.be.an('object'); + expect(document).to.have.property('squareBrackets[]InMiddle'); + expect(document).to.have.property('field[]WithMore'); + expect(document).to.have.property('start[]middle[]end'); } }); @@ -368,7 +402,16 @@ describe('Script Generation', () => { expect(result.success).to.equal(true); if (result.success) { - expect(result.script).to.contain('Array.from({length: 3}'); + const expectedReturnBlock = `return { + tags: Array.from({length: 3}, () => faker.lorem.word()) + };`; + expect(result.script).to.contain(expectedReturnBlock); + + // Test that the generated document code is executable + const document = testDocumentCodeExecution(result.script); + expect(document).to.be.an('object'); + expect(document).to.have.property('tags'); + expect(document.tags).to.be.an('array').with.length(3); } }); @@ -388,7 +431,16 @@ describe('Script Generation', () => { expect(result.success).to.equal(true); if (result.success) { - expect(result.script).to.contain('Array.from({length: 5}'); + const expectedReturnBlock = `return { + tags: Array.from({length: 5}, () => faker.lorem.word()) + };`; + expect(result.script).to.contain(expectedReturnBlock); + + // Test that the generated document code is executable + const document = testDocumentCodeExecution(result.script); + expect(document).to.be.an('object'); + expect(document).to.have.property('tags'); + expect(document.tags).to.be.an('array').with.length(5); } }); @@ -449,13 +501,20 @@ describe('Script Generation', () => { expect(result.success).to.equal(true); if (result.success) { - // Should have tags array with length 0 (empty array) - expect(result.script).to.contain('Array.from({length: 0}'); - // Should have categories array with length 2 - expect(result.script).to.contain('Array.from({length: 2}'); - // Verify both arrays are present - expect(result.script).to.contain('tags:'); - expect(result.script).to.contain('categories:'); + // 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()) + };`; + expect(result.script).to.contain(expectedReturnBlock); + + // Test that the generated document code is executable + const document = testDocumentCodeExecution(result.script); + expect(document).to.be.an('object'); + expect(document).to.have.property('tags'); + expect(document.tags).to.be.an('array').with.length(0); + expect(document).to.have.property('categories'); + expect(document.categories).to.be.an('array').with.length(2); } }); @@ -482,6 +541,9 @@ describe('Script Generation', () => { cube: Array.from({length: 3}, () => Array.from({length: 4}, () => Array.from({length: 2}, () => faker.number.float()))) };`; expect(result.script).to.contain(expectedReturnBlock); + + // Test that the generated document code is executable + testDocumentCodeExecution(result.script); } }); @@ -517,18 +579,22 @@ describe('Script Generation', () => { expect(result.success).to.equal(true); if (result.success) { - // Users array should have 2 elements - expect(result.script).to.contain('users: Array.from({length: 2}'); - // Each user's tags should have 3 elements - expect(result.script).to.contain('tags: Array.from({length: 3}'); - // Each user's posts should have 4 elements - expect(result.script).to.contain('posts: Array.from({length: 4}'); - // Each post's comments should have 5 elements - expect(result.script).to.contain('comments: Array.from({length: 5}'); - // Matrix should be 2x3 - expect(result.script).to.contain( - 'matrix: Array.from({length: 2}, () => Array.from({length: 3}' - ); + // Complex nested structure with custom array lengths + const expectedReturnBlock = `return { + users: Array.from({length: 2}, () => ({ + name: faker.person.fullName(), + 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()) + })) + })), + matrix: Array.from({length: 2}, () => Array.from({length: 3}, () => faker.number.int())) + };`; + expect(result.script).to.contain(expectedReturnBlock); + + // Test that the generated document code is executable + testDocumentCodeExecution(result.script); } }); }); @@ -551,7 +617,16 @@ describe('Script Generation', () => { expect(result.success).to.equal(true); if (result.success) { - expect(result.script).to.contain('faker.lorem.word()'); + const expectedReturnBlock = `return { + unknownField: faker.lorem.word() + };`; + expect(result.script).to.contain(expectedReturnBlock); + + // Test that the generated document code is executable + const document = testDocumentCodeExecution(result.script); + expect(document).to.be.an('object'); + expect(document).to.have.property('unknownField'); + expect(document.unknownField).to.be.a('string').and.not.be.empty; } }); @@ -572,7 +647,16 @@ describe('Script Generation', () => { expect(result.success).to.equal(true); if (result.success) { - expect(result.script).to.contain('faker.number.int()'); + const expectedReturnBlock = `return { + unknownNumber: faker.number.int() + };`; + expect(result.script).to.contain(expectedReturnBlock); + + // Test that the generated document code is executable + const document = testDocumentCodeExecution(result.script); + expect(document).to.be.an('object'); + expect(document).to.have.property('unknownNumber'); + expect(document.unknownNumber).to.be.a('number'); } }); @@ -594,6 +678,9 @@ describe('Script Generation', () => { expect(result.success).to.equal(true); if (result.success) { expect(result.script).to.contain('faker.date.recent()'); + + // Test that the generated document code is executable + testDocumentCodeExecution(result.script); } }); @@ -615,6 +702,9 @@ describe('Script Generation', () => { expect(result.success).to.equal(true); if (result.success) { expect(result.script).to.contain('faker.datatype.boolean()'); + + // Test that the generated document code is executable + testDocumentCodeExecution(result.script); } }); @@ -636,6 +726,9 @@ describe('Script Generation', () => { expect(result.success).to.equal(true); if (result.success) { expect(result.script).to.contain('faker.database.mongodbObjectId()'); + + // Test that the generated document code is executable + testDocumentCodeExecution(result.script); } }); @@ -657,6 +750,9 @@ describe('Script Generation', () => { expect(result.success).to.equal(true); if (result.success) { expect(result.script).to.contain('faker.number.float()'); + + // Test that the generated document code is executable + testDocumentCodeExecution(result.script); } }); @@ -678,6 +774,9 @@ describe('Script Generation', () => { expect(result.success).to.equal(true); if (result.success) { expect(result.script).to.contain('faker.lorem.word()'); + + // Test that the generated document code is executable + testDocumentCodeExecution(result.script); } }); }); @@ -701,6 +800,9 @@ describe('Script Generation', () => { expect(result.success).to.equal(true); if (result.success) { expect(result.script).to.contain('faker.person.firstName("male")'); + + // Test that the generated document code is executable + testDocumentCodeExecution(result.script); } }); @@ -722,27 +824,9 @@ describe('Script Generation', () => { expect(result.success).to.equal(true); if (result.success) { expect(result.script).to.contain('faker.number.int(18, 65)'); - } - }); - it('should handle boolean arguments', () => { - const schema = { - active: { - mongoType: 'boolean', - fakerMethod: 'datatype.boolean', - fakerArgs: [0.8], - }, - }; - - const result = generateScript(schema, { - databaseName: 'testdb', - collectionName: 'users', - documentCount: 1, - }); - - expect(result.success).to.equal(true); - if (result.success) { - expect(result.script).to.contain('faker.datatype.boolean(0.8)'); + // Test that the generated document code is executable + testDocumentCodeExecution(result.script); } }); @@ -766,10 +850,13 @@ describe('Script Generation', () => { expect(result.script).to.contain( 'faker.number.int({"min":0,"max":100})' ); + + // Test that the generated document code is executable + testDocumentCodeExecution(result.script); } }); - it('should handle JSON arguments', () => { + it('should handle JSON array arguments', () => { const schema = { color: { mongoType: 'string', @@ -789,6 +876,9 @@ describe('Script Generation', () => { expect(result.script).to.contain( "faker.helpers.arrayElement(['red', 'blue', 'green'])" ); + + // Test that the generated document code is executable + testDocumentCodeExecution(result.script); } }); @@ -810,6 +900,9 @@ describe('Script Generation', () => { expect(result.success).to.equal(true); if (result.success) { expect(result.script).to.contain('faker.lorem.words(5, true)'); + + // Test that the generated document code is executable + testDocumentCodeExecution(result.script); } }); @@ -817,8 +910,10 @@ describe('Script Generation', () => { const schema = { quote: { mongoType: 'string', - fakerMethod: 'lorem.sentence', - fakerArgs: ["It's a 'test' string"], + fakerMethod: 'helpers.arrayElement', + fakerArgs: [ + { json: '["It\'s a \'test\' string", "another option"]' }, + ], }, }; @@ -831,8 +926,11 @@ describe('Script Generation', () => { expect(result.success).to.equal(true); if (result.success) { expect(result.script).to.contain( - "faker.lorem.sentence(\"It's a 'test' string\")" + 'faker.helpers.arrayElement(["It\'s a \'test\' string", "another option"])' ); + + // Test that the generated document code is executable + testDocumentCodeExecution(result.script); } }); @@ -854,34 +952,9 @@ describe('Script Generation', () => { expect(result.success).to.equal(true); if (result.success) { expect(result.script).to.contain('faker.string.uuid()'); - } - }); - - it('should safely handle complex string arguments with newlines and special characters', () => { - const schema = { - complexString: { - mongoType: 'string', - fakerMethod: 'lorem.sentence', - fakerArgs: [ - 'Line 1\nLine 2\tTabbed\r\nWith "quotes" and \'apostrophes\'', - ], - }, - }; - - const result = generateScript(schema, { - databaseName: 'testdb', - collectionName: 'test', - documentCount: 1, - }); - expect(result.success).to.equal(true); - if (result.success) { - // Should use JSON.stringify for safe string serialization - expect(result.script).to.contain( - 'faker.lorem.sentence("Line 1\\nLine 2\\tTabbed\\r\\nWith \\"quotes\\" and \'apostrophes\'")' - ); - // Should not contain unescaped newlines that would break JS - expect(result.script).not.to.contain('Line 1\nLine 2'); + // Test that the generated document code is executable + testDocumentCodeExecution(result.script); } }); }); @@ -902,6 +975,9 @@ describe('Script Generation', () => { if (result.success) { expect(result.script).to.contain('faker.person.fullName()'); expect(result.script).not.to.contain('Math.random()'); + + // Test that the generated document code is executable + testDocumentCodeExecution(result.script); } }); @@ -920,6 +996,9 @@ describe('Script Generation', () => { if (result.success) { expect(result.script).to.contain('faker.person.fullName()'); expect(result.script).not.to.contain('Math.random()'); + + // Test that the generated document code is executable + testDocumentCodeExecution(result.script); } }); @@ -954,10 +1033,16 @@ describe('Script Generation', () => { expect(result.success).to.equal(true); if (result.success) { // All fields should be treated as probability 1.0 (always present) - expect(result.script).to.contain('field1: faker.lorem.word()'); - expect(result.script).to.contain('field2: faker.lorem.word()'); - expect(result.script).to.contain('field3: faker.lorem.word()'); + const expectedReturnBlock = `return { + field1: faker.lorem.word(), + field2: faker.lorem.word(), + field3: faker.lorem.word() + };`; + expect(result.script).to.contain(expectedReturnBlock); expect(result.script).not.to.contain('Math.random()'); + + // Test that the generated document code is executable + testDocumentCodeExecution(result.script); } }); @@ -974,9 +1059,13 @@ describe('Script Generation', () => { expect(result.success).to.equal(true); if (result.success) { - expect(result.script).to.contain( - '...(Math.random() < 0.7 ? { optionalField: faker.lorem.word() } : {})' - ); + const expectedReturnBlock = `return { + ...(Math.random() < 0.7 ? { optionalField: faker.lorem.word() } : {}) + };`; + expect(result.script).to.contain(expectedReturnBlock); + + // Test that the generated document code is executable + testDocumentCodeExecution(result.script); } }); @@ -996,27 +1085,22 @@ describe('Script Generation', () => { expect(result.success).to.equal(true); if (result.success) { - // Always present (probability 1.0) - expect(result.script).to.contain('faker.person.fullName()'); + const expectedReturnBlock = `return { + alwaysPresent: faker.person.fullName(), + ...(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); expect(result.script).not.to.contain( 'Math.random() < 1 ? { alwaysPresent:' ); - - // Sometimes present (probability 0.8) - expect(result.script).to.contain( - '...(Math.random() < 0.8 ? { sometimesPresent: faker.internet.email() } : {})' - ); - - // Rarely present (probability 0.2) - expect(result.script).to.contain( - '...(Math.random() < 0.2 ? { rarelyPresent: faker.phone.number() } : {})' - ); - - // Default probability (undefined = 1.0) - expect(result.script).to.contain('faker.lorem.word()'); expect(result.script).not.to.contain( 'Math.random() < 1 ? { defaultProbability:' ); + + // Test that the generated document code is executable + testDocumentCodeExecution(result.script); } }); @@ -1041,6 +1125,9 @@ describe('Script Generation', () => { expect(result.script).to.contain( '...(Math.random() < 0.9 ? { conditionalAge: faker.number.int(18, 65) } : {})' ); + + // Test that the generated document code is executable + testDocumentCodeExecution(result.script); } }); @@ -1065,6 +1152,9 @@ describe('Script Generation', () => { expect(result.script).to.contain( '...(Math.random() < 0.5 ? { unknownField: 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 548a4119b2d..f2f331aa5c9 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 @@ -344,11 +344,15 @@ function renderDocumentCode( if (probability < 1.0) { // Use Math.random for conditional field inclusion documentFields.push( - `${fieldIndent}...(Math.random() < ${probability} ? { ${fieldName}: ${fakerCall} } : {})` + `${fieldIndent}...(Math.random() < ${probability} ? { ${formatFieldName( + fieldName + )}: ${fakerCall} } : {})` ); } else { // Normal field inclusion - documentFields.push(`${fieldIndent}${fieldName}: ${fakerCall}`); + documentFields.push( + `${fieldIndent}${formatFieldName(fieldName)}: ${fakerCall}` + ); } } else if ('type' in value && value.type === 'array') { // It's an array @@ -359,7 +363,9 @@ function renderDocumentCode( arrayLengthMap, 0 // Start at dimension 0 ); - documentFields.push(`${fieldIndent}${fieldName}: ${arrayCode}`); + documentFields.push( + `${fieldIndent}${formatFieldName(fieldName)}: ${arrayCode}` + ); } else { // It's a nested object: recursive call @@ -376,7 +382,9 @@ function renderDocumentCode( indent + INDENT_SIZE, nestedArrayLengthMap ); - documentFields.push(`${fieldIndent}${fieldName}: ${nestedCode}`); + documentFields.push( + `${fieldIndent}${formatFieldName(fieldName)}: ${nestedCode}` + ); } } @@ -388,6 +396,22 @@ function renderDocumentCode( return `{\n${documentFields.join(',\n')}\n${closingBraceIndent}}`; } +/** + * Formats a field name for use in JavaScript object literal syntax. + * Only quotes field names that need it, using JSON.stringify for proper escaping. + */ +function formatFieldName(fieldName: string): string { + // If it's a valid JavaScript identifier, don't quote it + const isValidIdentifier = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(fieldName); + + if (isValidIdentifier) { + return fieldName; + } else { + // Use JSON.stringify for proper escaping of special characters + return JSON.stringify(fieldName); + } +} + /** * Generate array code */ From 2d856202aa724c9fbbee53b9cb196e3c825e7b16 Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Mon, 15 Sep 2025 12:51:20 -0400 Subject: [PATCH 24/31] Include more defaults for all BSON types --- .../script-generation-utils.spec.ts | 150 ++++++++++++++++++ .../script-generation-utils.ts | 56 +++++++ 2 files changed, 206 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 4d7a0e6991a..3964ff1d2e3 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 @@ -779,6 +779,156 @@ describe('Script Generation', () => { testDocumentCodeExecution(result.script); } }); + + it('should use default faker method for timestamp fields', () => { + const schema = { + timestampField: { + mongoType: 'timestamp', + fakerMethod: 'unrecognized', + fakerArgs: [], + }, + }; + + const result = generateScript(schema, { + databaseName: 'testdb', + collectionName: 'test', + documentCount: 1, + }); + + expect(result.success).to.equal(true); + if (result.success) { + expect(result.script).to.contain('faker.date.recent()'); + + // Test that the generated document code is executable + testDocumentCodeExecution(result.script); + } + }); + + it('should use default faker method for array fields', () => { + const schema = { + arrayField: { + mongoType: 'array', + fakerMethod: 'unrecognized', + fakerArgs: [], + }, + }; + + const result = generateScript(schema, { + databaseName: 'testdb', + collectionName: 'test', + documentCount: 1, + }); + + expect(result.success).to.equal(true); + if (result.success) { + expect(result.script).to.contain('faker.lorem.word()'); + + // Test that the generated document code is executable + testDocumentCodeExecution(result.script); + } + }); + + it('should use default faker method for regex fields', () => { + const schema = { + regexField: { + mongoType: 'regex', + fakerMethod: 'unrecognized', + fakerArgs: [], + }, + }; + + const result = generateScript(schema, { + databaseName: 'testdb', + collectionName: 'test', + documentCount: 1, + }); + + expect(result.success).to.equal(true); + if (result.success) { + expect(result.script).to.contain('faker.lorem.word()'); + + // Test that the generated document code is executable + testDocumentCodeExecution(result.script); + } + }); + + it('should use default faker method for javascript fields', () => { + const schema = { + jsField: { + mongoType: 'javascript', + fakerMethod: 'unrecognized', + fakerArgs: [], + }, + }; + + const result = generateScript(schema, { + databaseName: 'testdb', + collectionName: 'test', + documentCount: 1, + }); + + expect(result.success).to.equal(true); + if (result.success) { + expect(result.script).to.contain('faker.lorem.sentence()'); + + // Test that the generated document code is executable + testDocumentCodeExecution(result.script); + } + }); + + it('should handle null fields by returning literal null', () => { + const schema = { + nullField: { + mongoType: 'null', + fakerMethod: 'unrecognized', + fakerArgs: [], + }, + }; + + const result = generateScript(schema, { + databaseName: 'testdb', + collectionName: 'test', + documentCount: 1, + }); + + expect(result.success).to.equal(true); + if (result.success) { + expect(result.script).to.contain('nullField: null'); + + // Test that the generated document code is executable + const document = testDocumentCodeExecution(result.script); + expect(document).to.be.an('object'); + expect(document).to.have.property('nullField'); + expect(document.nullField).to.be.null; + } + }); + + it('should handle undefined fields by returning literal undefined', () => { + const schema = { + undefinedField: { + mongoType: 'undefined', + fakerMethod: 'unrecognized', + fakerArgs: [], + }, + }; + + const result = generateScript(schema, { + databaseName: 'testdb', + collectionName: 'test', + documentCount: 1, + }); + + expect(result.success).to.equal(true); + if (result.success) { + expect(result.script).to.contain('undefinedField: undefined'); + + // Test that the generated document code is executable + const document = testDocumentCodeExecution(result.script); + expect(document).to.be.an('object'); + expect(document).to.have.property('undefinedField'); + expect(document.undefinedField).to.be.undefined; + } + }); }); describe('Faker Arguments', () => { 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 f2f331aa5c9..f9d9b39ed18 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 @@ -469,6 +469,12 @@ function renderArrayCode( * Generate faker.js call from field mapping */ function generateFakerCall(mapping: FieldMapping): string { + if (mapping.mongoType === 'null') { + return 'null'; + } + if (mapping.mongoType === 'undefined') { + return 'undefined'; + } const method = mapping.fakerMethod === 'unrecognized' ? getDefaultFakerMethod(mapping.mongoType) @@ -483,23 +489,73 @@ function generateFakerCall(mapping: FieldMapping): string { */ export function getDefaultFakerMethod(mongoType: string): string { switch (mongoType.toLowerCase()) { + // String types case 'string': return 'lorem.word'; + + // Numeric types case 'number': case 'int32': case 'int64': + case 'long': return 'number.int'; case 'double': case 'decimal128': return 'number.float'; + + // Date and time types case 'date': + case 'timestamp': return 'date.recent'; + + // Object identifier case 'objectid': return 'database.mongodbObjectId'; + + // Boolean case 'boolean': + case 'bool': return 'datatype.boolean'; + + // Binary case 'binary': + case 'bindata': return 'string.hexadecimal'; + + // Array + case 'array': + return 'lorem.word'; + + // Object/Document type + case 'object': + case 'document': + return 'lorem.word'; + + // Regular expression + case 'regex': + case 'regexp': + return 'lorem.word'; + + // JavaScript code + case 'javascript': + case 'code': + return 'lorem.sentence'; + + // MinKey and MaxKey + case 'minkey': + return 'number.int'; + case 'maxkey': + return 'number.int'; + + // Symbol (deprecated) + case 'symbol': + return 'lorem.word'; + + // DBPointer (deprecated) + case 'dbpointer': + return 'database.mongodbObjectId'; + + // Default fallback default: return 'lorem.word'; } From 7d1d32ccd399e21edd7d10f1b9d7aef9ebba55b7 Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Mon, 15 Sep 2025 12:57:47 -0400 Subject: [PATCH 25/31] More tests --- .../script-generation-utils.spec.ts | 102 +++++++++--------- .../script-generation-utils.ts | 1 + 2 files changed, 51 insertions(+), 52 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 3964ff1d2e3..6458e49a932 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 @@ -637,6 +637,31 @@ describe('Script Generation', () => { fakerMethod: 'unrecognized', fakerArgs: [], }, + unknownInt: { + mongoType: 'int', + fakerMethod: 'unrecognized', + fakerArgs: [], + }, + unknownInt32: { + mongoType: 'int32', + fakerMethod: 'unrecognized', + fakerArgs: [], + }, + unknownInt64: { + mongoType: 'int64', + fakerMethod: 'unrecognized', + fakerArgs: [], + }, + unknownLong: { + mongoType: 'long', + fakerMethod: 'unrecognized', + fakerArgs: [], + }, + unknownDecimal128: { + mongoType: 'decimal128', + fakerMethod: 'unrecognized', + fakerArgs: [], + }, }; const result = generateScript(schema, { @@ -647,16 +672,37 @@ describe('Script Generation', () => { expect(result.success).to.equal(true); if (result.success) { - const expectedReturnBlock = `return { - unknownNumber: faker.number.int() - };`; - expect(result.script).to.contain(expectedReturnBlock); + // Check that integer types use faker.number.int() + expect(result.script).to.contain('unknownNumber: faker.number.int()'); + expect(result.script).to.contain('unknownInt: faker.number.int()'); + expect(result.script).to.contain('unknownInt32: faker.number.int()'); + expect(result.script).to.contain('unknownInt64: faker.number.int()'); + expect(result.script).to.contain('unknownLong: faker.number.int()'); + + // Check that decimal128 uses faker.number.float() + expect(result.script).to.contain( + 'unknownDecimal128: faker.number.float()' + ); // Test that the generated document code is executable const document = testDocumentCodeExecution(result.script); expect(document).to.be.an('object'); + + // Validate integer fields expect(document).to.have.property('unknownNumber'); expect(document.unknownNumber).to.be.a('number'); + expect(document).to.have.property('unknownInt'); + expect(document.unknownInt32).to.be.a('number'); + expect(document).to.have.property('unknownInt32'); + expect(document.unknownInt32).to.be.a('number'); + expect(document).to.have.property('unknownInt64'); + expect(document.unknownInt64).to.be.a('number'); + expect(document).to.have.property('unknownLong'); + expect(document.unknownLong).to.be.a('number'); + + // Validate decimal field + expect(document).to.have.property('unknownDecimal128'); + expect(document.unknownDecimal128).to.be.a('number'); } }); @@ -732,30 +778,6 @@ describe('Script Generation', () => { } }); - it('should use default faker method for unrecognized double fields', () => { - const schema = { - unknownDouble: { - mongoType: 'double', - fakerMethod: 'unrecognized', - fakerArgs: [], - }, - }; - - const result = generateScript(schema, { - databaseName: 'testdb', - collectionName: 'test', - documentCount: 1, - }); - - expect(result.success).to.equal(true); - if (result.success) { - expect(result.script).to.contain('faker.number.float()'); - - // Test that the generated document code is executable - testDocumentCodeExecution(result.script); - } - }); - it('should fall back to lorem.word for unknown MongoDB types', () => { const schema = { unknownType: { @@ -804,30 +826,6 @@ describe('Script Generation', () => { } }); - it('should use default faker method for array fields', () => { - const schema = { - arrayField: { - mongoType: 'array', - fakerMethod: 'unrecognized', - fakerArgs: [], - }, - }; - - const result = generateScript(schema, { - databaseName: 'testdb', - collectionName: 'test', - documentCount: 1, - }); - - expect(result.success).to.equal(true); - if (result.success) { - expect(result.script).to.contain('faker.lorem.word()'); - - // Test that the generated document code is executable - testDocumentCodeExecution(result.script); - } - }); - it('should use default faker method for regex fields', () => { const schema = { regexField: { 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 f9d9b39ed18..c5807a6fd14 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 @@ -495,6 +495,7 @@ export function getDefaultFakerMethod(mongoType: string): string { // Numeric types case 'number': + case 'int': case 'int32': case 'int64': case 'long': From 984772181650cb4348328f57c77eb549aed72f16 Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Mon, 15 Sep 2025 13:17:34 -0400 Subject: [PATCH 26/31] Faker version --- package-lock.json | 23 ----------------------- packages/compass-collection/package.json | 1 - 2 files changed, 24 deletions(-) diff --git a/package-lock.json b/package-lock.json index a18b8894ecb..d2745fb7bd1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6031,22 +6031,6 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/@faker-js/faker": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.4.1.tgz", - "integrity": "sha512-XQ3cU+Q8Uqmrbf2e0cIC/QN43sTBSC8KF12u29Mb47tWrt2hAgBXSgpZMj4Ao8Uk0iJcU99QsOCaIL8934obCg==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/fakerjs" - } - ], - "license": "MIT", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0", - "npm": ">=6.14.13" - } - }, "node_modules/@fastify/busboy": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", @@ -47735,7 +47719,6 @@ "version": "4.71.1", "license": "SSPL", "dependencies": { - "@faker-js/faker": "^8.4.1", "@mongodb-js/compass-app-registry": "^9.4.21", "@mongodb-js/compass-app-stores": "^7.58.1", "@mongodb-js/compass-components": "^1.50.1", @@ -57983,11 +57966,6 @@ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==" }, - "@faker-js/faker": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.4.1.tgz", - "integrity": "sha512-XQ3cU+Q8Uqmrbf2e0cIC/QN43sTBSC8KF12u29Mb47tWrt2hAgBXSgpZMj4Ao8Uk0iJcU99QsOCaIL8934obCg==" - }, "@fastify/busboy": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", @@ -61236,7 +61214,6 @@ "@mongodb-js/compass-collection": { "version": "file:packages/compass-collection", "requires": { - "@faker-js/faker": "^8.4.1", "@mongodb-js/compass-app-registry": "^9.4.21", "@mongodb-js/compass-app-stores": "^7.58.1", "@mongodb-js/compass-components": "^1.50.1", diff --git a/packages/compass-collection/package.json b/packages/compass-collection/package.json index 76c8bf25be9..31bdfbcd877 100644 --- a/packages/compass-collection/package.json +++ b/packages/compass-collection/package.json @@ -48,7 +48,6 @@ "reformat": "npm run eslint . -- --fix && npm run prettier -- --write ." }, "dependencies": { - "@faker-js/faker": "^8.4.1", "@mongodb-js/compass-app-registry": "^9.4.21", "@mongodb-js/compass-app-stores": "^7.58.1", "@mongodb-js/compass-components": "^1.50.1", From 834979cb9a6f2292ac65bb5c450f1c233739f63c Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Mon, 15 Sep 2025 13:22:25 -0400 Subject: [PATCH 27/31] Faker version --- package-lock.json | 23 +++++++++++++++++++++++ packages/compass-collection/package.json | 1 + 2 files changed, 24 insertions(+) diff --git a/package-lock.json b/package-lock.json index 6cb305f884f..4a3b97138c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6031,6 +6031,22 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@faker-js/faker": { + "version": "9.9.0", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.9.0.tgz", + "integrity": "sha512-OEl393iCOoo/z8bMezRlJu+GlRGlsKbUAN7jKB6LhnKoqKve5DXRpalbItIIcwnCjs1k/FOPjFzcA6Qn+H+YbA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/fakerjs" + } + ], + "license": "MIT", + "engines": { + "node": ">=18.0.0", + "npm": ">=9.0.0" + } + }, "node_modules/@fast-csv/parse": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/@fast-csv/parse/-/parse-5.0.5.tgz", @@ -47761,6 +47777,7 @@ "version": "4.72.0", "license": "SSPL", "dependencies": { + "@faker-js/faker": "^9.0.0", "@mongodb-js/compass-app-registry": "^9.4.22", "@mongodb-js/compass-app-stores": "^7.59.0", "@mongodb-js/compass-components": "^1.51.0", @@ -58080,6 +58097,11 @@ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==" }, + "@faker-js/faker": { + "version": "9.9.0", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.9.0.tgz", + "integrity": "sha512-OEl393iCOoo/z8bMezRlJu+GlRGlsKbUAN7jKB6LhnKoqKve5DXRpalbItIIcwnCjs1k/FOPjFzcA6Qn+H+YbA==" + }, "@fast-csv/parse": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/@fast-csv/parse/-/parse-5.0.5.tgz", @@ -61347,6 +61369,7 @@ "@mongodb-js/compass-collection": { "version": "file:packages/compass-collection", "requires": { + "@faker-js/faker": "^9.0.0", "@mongodb-js/compass-app-registry": "^9.4.22", "@mongodb-js/compass-app-stores": "^7.59.0", "@mongodb-js/compass-components": "^1.51.0", diff --git a/packages/compass-collection/package.json b/packages/compass-collection/package.json index 2b85d985f9d..7eb63f39ee8 100644 --- a/packages/compass-collection/package.json +++ b/packages/compass-collection/package.json @@ -48,6 +48,7 @@ "reformat": "npm run eslint . -- --fix && npm run prettier -- --write ." }, "dependencies": { + "@faker-js/faker": "^9.0.0", "@mongodb-js/compass-app-registry": "^9.4.22", "@mongodb-js/compass-app-stores": "^7.59.0", "@mongodb-js/compass-components": "^1.51.0", From 4d64eff024ce37634f93b560d9dce4d261e829cf Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Tue, 16 Sep 2025 18:22:29 -0400 Subject: [PATCH 28/31] Rename type --- .../script-generation-utils.spec.ts | 7 +++++-- .../script-generation-utils.ts | 20 +++++++++---------- 2 files changed, 15 insertions(+), 12 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 6458e49a932..9c4fd705195 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,9 @@ import { expect } from 'chai'; import { faker } from '@faker-js/faker/locale/en'; -import { generateScript, type FieldMapping } from './script-generation-utils'; +import { + generateScript, + type FakerFieldMapping, +} from './script-generation-utils'; /** * Helper function to test that generated document code is executable @@ -41,7 +44,7 @@ describe('Script Generation', () => { const createFieldMapping = ( fakerMethod: string, probability?: number - ): FieldMapping => ({ + ): FakerFieldMapping => ({ mongoType: 'String', fakerMethod, fakerArgs: [], 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 c5807a6fd14..34bf099dbcf 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 @@ -3,7 +3,7 @@ export type FakerArg = string | number | boolean | { json: string }; const DEFAULT_ARRAY_LENGTH = 3; const INDENT_SIZE = 2; -export interface FieldMapping { +export interface FakerFieldMapping { mongoType: string; fakerMethod: string; fakerArgs: FakerArg[]; @@ -35,21 +35,21 @@ export type ScriptResult = type DocumentStructure = { [fieldName: string]: - | FieldMapping // Leaf: actual data field + | FakerFieldMapping // Leaf: actual data field | DocumentStructure // Object: nested fields | ArrayStructure; // Array: repeated elements }; interface ArrayStructure { type: 'array'; - elementType: FieldMapping | DocumentStructure | ArrayStructure; + elementType: FakerFieldMapping | DocumentStructure | ArrayStructure; } /** * Entry point method: Generate the final script */ export function generateScript( - schema: Record, + schema: Record, options: ScriptOptions ): ScriptResult { try { @@ -173,7 +173,7 @@ function parseFieldPath(fieldPath: string): string[] { * Build the document structure from all field paths */ function buildDocumentStructure( - schema: Record + schema: Record ): DocumentStructure { const result: DocumentStructure = {}; @@ -192,7 +192,7 @@ function buildDocumentStructure( function insertIntoStructure( structure: DocumentStructure, pathParts: string[], - mapping: FieldMapping + mapping: FakerFieldMapping ): void { if (pathParts.length === 0) { throw new Error('Cannot insert field mapping: empty path parts array'); @@ -317,7 +317,7 @@ function renderDocumentCode( arrayLengthMap: ArrayLengthMap = {} ): string { // For each field in structure: - // - If FieldMapping: generate faker call + // - If FakerFieldMapping: generate faker call // - If DocumentStructure: generate nested object // - If ArrayStructure: generate array @@ -328,7 +328,7 @@ function renderDocumentCode( for (const [fieldName, value] of Object.entries(structure)) { if ('mongoType' in value) { // It's a field mapping - const mapping = value as FieldMapping; + const mapping = value as FakerFieldMapping; const fakerCall = generateFakerCall(mapping); // Default to 1.0 for invalid probability values let probability = 1.0; @@ -438,7 +438,7 @@ function renderArrayCode( if ('mongoType' in elementType) { // Array of primitives - const fakerCall = generateFakerCall(elementType as FieldMapping); + 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 @@ -468,7 +468,7 @@ function renderArrayCode( /** * Generate faker.js call from field mapping */ -function generateFakerCall(mapping: FieldMapping): string { +function generateFakerCall(mapping: FakerFieldMapping): string { if (mapping.mongoType === 'null') { return 'null'; } From 9da51cf3651a1466d43b949f59752d208b54d380 Mon Sep 17 00:00:00 2001 From: Jacob Lu <43422771+jcobis@users.noreply.github.com> Date: Tue, 16 Sep 2025 18:23:49 -0400 Subject: [PATCH 29/31] Regex alternative Co-authored-by: Anna Henningsen --- .../mock-data-generator-modal/script-generation-utils.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 9c4fd705195..a862ae9c782 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 @@ -19,7 +19,7 @@ function testDocumentCodeExecution(script: string): any { // The "{ ... }" part is the document structure // Extract the return statement from the generateDocument function - const returnMatch = script.match(/return ([\s\S]*?);[\s]*\}/); + const returnMatch = script.match(/return (.*?);\s*\}/s); expect(returnMatch, 'Should contain return statement').to.not.be.null; // Get the document structure expression (everything between "return" and ";") From 64b659c14190918a353cc3fc8bca49cb223cb8bb Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Tue, 16 Sep 2025 18:28:02 -0400 Subject: [PATCH 30/31] Escape names --- .../mock-data-generator-modal/script-generation-utils.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 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 34bf099dbcf..315ed6ce993 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 @@ -62,7 +62,9 @@ export function generateScript( ); const script = `// Mock Data Generator Script -// Generated for collection: ${options.databaseName}.${options.collectionName} +// Generated for collection: ${JSON.stringify( + options.databaseName + )}.${JSON.stringify(options.collectionName)} // Document count: ${options.documentCount} const { faker } = require('@faker-js/faker'); @@ -86,9 +88,9 @@ db.getCollection(${JSON.stringify( options.collectionName )}).insertMany(documents); -console.log(\`Successfully inserted \${documents.length} documents into ${ +console.log(\`Successfully inserted \${documents.length} documents into ${JSON.stringify( options.databaseName - }.${options.collectionName}\`);`; + )}.${JSON.stringify(options.collectionName)}\`);`; return { script, From 2144babfba7f2cf6de182b90bdf00ae32aa2e2df Mon Sep 17 00:00:00 2001 From: Jacob Samuel Lu Date: Wed, 17 Sep 2025 11:40:29 -0400 Subject: [PATCH 31/31] Type mongoType, ignore null and undefined --- .../script-generation-utils.spec.ts | 108 +++++------------- .../script-generation-utils.ts | 64 ++++------- 2 files changed, 49 insertions(+), 123 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 a862ae9c782..b7c24288dcf 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 @@ -45,7 +45,7 @@ describe('Script Generation', () => { fakerMethod: string, probability?: number ): FakerFieldMapping => ({ - mongoType: 'String', + mongoType: 'String' as const, fakerMethod, fakerArgs: [], ...(probability !== undefined && { probability }), @@ -606,7 +606,7 @@ describe('Script Generation', () => { it('should use default faker method for unrecognized string fields', () => { const schema = { unknownField: { - mongoType: 'string', + mongoType: 'String' as const, fakerMethod: 'unrecognized', fakerArgs: [], }, @@ -636,32 +636,32 @@ describe('Script Generation', () => { it('should use default faker method for unrecognized number fields', () => { const schema = { unknownNumber: { - mongoType: 'number', + mongoType: 'Number' as const, fakerMethod: 'unrecognized', fakerArgs: [], }, unknownInt: { - mongoType: 'int', + mongoType: 'Int32' as const, fakerMethod: 'unrecognized', fakerArgs: [], }, unknownInt32: { - mongoType: 'int32', + mongoType: 'Int32' as const, fakerMethod: 'unrecognized', fakerArgs: [], }, unknownInt64: { - mongoType: 'int64', + mongoType: 'Long' as const, fakerMethod: 'unrecognized', fakerArgs: [], }, unknownLong: { - mongoType: 'long', + mongoType: 'Long' as const, fakerMethod: 'unrecognized', fakerArgs: [], }, unknownDecimal128: { - mongoType: 'decimal128', + mongoType: 'Decimal128' as const, fakerMethod: 'unrecognized', fakerArgs: [], }, @@ -712,7 +712,7 @@ describe('Script Generation', () => { it('should use default faker method for unrecognized date fields', () => { const schema = { unknownDate: { - mongoType: 'date', + mongoType: 'Date' as const, fakerMethod: 'unrecognized', fakerArgs: [], }, @@ -736,7 +736,7 @@ describe('Script Generation', () => { it('should use default faker method for unrecognized boolean fields', () => { const schema = { unknownBool: { - mongoType: 'boolean', + mongoType: 'Boolean' as const, fakerMethod: 'unrecognized', fakerArgs: [], }, @@ -760,7 +760,7 @@ describe('Script Generation', () => { it('should use default faker method for unrecognized ObjectId fields', () => { const schema = { unknownId: { - mongoType: 'objectid', + mongoType: 'ObjectId' as const, fakerMethod: 'unrecognized', fakerArgs: [], }, @@ -784,7 +784,7 @@ describe('Script Generation', () => { it('should fall back to lorem.word for unknown MongoDB types', () => { const schema = { unknownType: { - mongoType: 'unknownType', + mongoType: 'String' as const, fakerMethod: 'unrecognized', fakerArgs: [], }, @@ -808,7 +808,7 @@ describe('Script Generation', () => { it('should use default faker method for timestamp fields', () => { const schema = { timestampField: { - mongoType: 'timestamp', + mongoType: 'Timestamp' as const, fakerMethod: 'unrecognized', fakerArgs: [], }, @@ -832,7 +832,7 @@ describe('Script Generation', () => { it('should use default faker method for regex fields', () => { const schema = { regexField: { - mongoType: 'regex', + mongoType: 'RegExp' as const, fakerMethod: 'unrecognized', fakerArgs: [], }, @@ -856,7 +856,7 @@ describe('Script Generation', () => { it('should use default faker method for javascript fields', () => { const schema = { jsField: { - mongoType: 'javascript', + mongoType: 'Code' as const, fakerMethod: 'unrecognized', fakerArgs: [], }, @@ -876,67 +876,13 @@ describe('Script Generation', () => { testDocumentCodeExecution(result.script); } }); - - it('should handle null fields by returning literal null', () => { - const schema = { - nullField: { - mongoType: 'null', - fakerMethod: 'unrecognized', - fakerArgs: [], - }, - }; - - const result = generateScript(schema, { - databaseName: 'testdb', - collectionName: 'test', - documentCount: 1, - }); - - expect(result.success).to.equal(true); - if (result.success) { - expect(result.script).to.contain('nullField: null'); - - // Test that the generated document code is executable - const document = testDocumentCodeExecution(result.script); - expect(document).to.be.an('object'); - expect(document).to.have.property('nullField'); - expect(document.nullField).to.be.null; - } - }); - - it('should handle undefined fields by returning literal undefined', () => { - const schema = { - undefinedField: { - mongoType: 'undefined', - fakerMethod: 'unrecognized', - fakerArgs: [], - }, - }; - - const result = generateScript(schema, { - databaseName: 'testdb', - collectionName: 'test', - documentCount: 1, - }); - - expect(result.success).to.equal(true); - if (result.success) { - expect(result.script).to.contain('undefinedField: undefined'); - - // Test that the generated document code is executable - const document = testDocumentCodeExecution(result.script); - expect(document).to.be.an('object'); - expect(document).to.have.property('undefinedField'); - expect(document.undefinedField).to.be.undefined; - } - }); }); describe('Faker Arguments', () => { it('should handle string arguments', () => { const schema = { name: { - mongoType: 'string', + mongoType: 'String' as const, fakerMethod: 'person.firstName', fakerArgs: ['male'], }, @@ -960,7 +906,7 @@ describe('Script Generation', () => { it('should handle number arguments', () => { const schema = { age: { - mongoType: 'number', + mongoType: 'Number' as const, fakerMethod: 'number.int', fakerArgs: [18, 65], }, @@ -984,7 +930,7 @@ describe('Script Generation', () => { it('should handle JSON object arguments', () => { const schema = { score: { - mongoType: 'number', + mongoType: 'Number' as const, fakerMethod: 'number.int', fakerArgs: [{ json: '{"min":0,"max":100}' }], }, @@ -1010,7 +956,7 @@ describe('Script Generation', () => { it('should handle JSON array arguments', () => { const schema = { color: { - mongoType: 'string', + mongoType: 'String' as const, fakerMethod: 'helpers.arrayElement', fakerArgs: [{ json: "['red', 'blue', 'green']" }], }, @@ -1036,7 +982,7 @@ describe('Script Generation', () => { it('should handle mixed argument types', () => { const schema = { description: { - mongoType: 'string', + mongoType: 'String' as const, fakerMethod: 'lorem.words', fakerArgs: [5, true], }, @@ -1060,7 +1006,7 @@ describe('Script Generation', () => { it('should safely handle quotes and special characters in string arguments', () => { const schema = { quote: { - mongoType: 'string', + mongoType: 'String' as const, fakerMethod: 'helpers.arrayElement', fakerArgs: [ { json: '["It\'s a \'test\' string", "another option"]' }, @@ -1088,7 +1034,7 @@ describe('Script Generation', () => { it('should handle empty arguments array', () => { const schema = { id: { - mongoType: 'string', + mongoType: 'String' as const, fakerMethod: 'string.uuid', fakerArgs: [], }, @@ -1156,19 +1102,19 @@ describe('Script Generation', () => { it('should default invalid probability to 1.0', () => { const schema = { field1: { - mongoType: 'string', + mongoType: 'String' as const, fakerMethod: 'lorem.word', fakerArgs: [], probability: 1.5, // Invalid - should default to 1.0 }, field2: { - mongoType: 'string', + mongoType: 'String' as const, fakerMethod: 'lorem.word', fakerArgs: [], probability: -0.5, // Invalid - should default to 1.0 }, field3: { - mongoType: 'string', + mongoType: 'String' as const, fakerMethod: 'lorem.word', fakerArgs: [], probability: 'invalid' as any, // Invalid - should default to 1.0 @@ -1258,7 +1204,7 @@ describe('Script Generation', () => { it('should handle probability with faker arguments', () => { const schema = { conditionalAge: { - mongoType: 'number', + mongoType: 'Number' as const, fakerMethod: 'number.int', fakerArgs: [18, 65], probability: 0.9, @@ -1285,7 +1231,7 @@ describe('Script Generation', () => { it('should handle probability with unrecognized fields', () => { const schema = { unknownField: { - mongoType: 'string', + mongoType: 'String' as const, fakerMethod: 'unrecognized', fakerArgs: [], probability: 0.5, 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 315ed6ce993..1fcc7b09f8f 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,12 @@ +import type { MongoDBFieldType } from '../../schema-analysis-types'; + export type FakerArg = string | number | boolean | { json: string }; const DEFAULT_ARRAY_LENGTH = 3; const INDENT_SIZE = 2; export interface FakerFieldMapping { - mongoType: string; + mongoType: MongoDBFieldType; fakerMethod: string; fakerArgs: FakerArg[]; probability?: number; // 0.0 - 1.0 frequency of field (defaults to 1.0) @@ -471,12 +473,6 @@ function renderArrayCode( * Generate faker.js call from field mapping */ function generateFakerCall(mapping: FakerFieldMapping): string { - if (mapping.mongoType === 'null') { - return 'null'; - } - if (mapping.mongoType === 'undefined') { - return 'undefined'; - } const method = mapping.fakerMethod === 'unrecognized' ? getDefaultFakerMethod(mapping.mongoType) @@ -489,73 +485,57 @@ function generateFakerCall(mapping: FakerFieldMapping): string { /** * Gets default faker method for unrecognized fields based on MongoDB type */ -export function getDefaultFakerMethod(mongoType: string): string { - switch (mongoType.toLowerCase()) { +export function getDefaultFakerMethod(mongoType: MongoDBFieldType): string { + switch (mongoType) { // String types - case 'string': + case 'String': return 'lorem.word'; // Numeric types - case 'number': - case 'int': - case 'int32': - case 'int64': - case 'long': + case 'Number': + case 'Int32': + case 'Long': return 'number.int'; - case 'double': - case 'decimal128': + case 'Decimal128': return 'number.float'; // Date and time types - case 'date': - case 'timestamp': + case 'Date': + case 'Timestamp': return 'date.recent'; // Object identifier - case 'objectid': + case 'ObjectId': return 'database.mongodbObjectId'; // Boolean - case 'boolean': - case 'bool': + case 'Boolean': return 'datatype.boolean'; // Binary - case 'binary': - case 'bindata': + case 'Binary': return 'string.hexadecimal'; - // Array - case 'array': - return 'lorem.word'; - - // Object/Document type - case 'object': - case 'document': - return 'lorem.word'; - // Regular expression - case 'regex': - case 'regexp': + case 'RegExp': return 'lorem.word'; // JavaScript code - case 'javascript': - case 'code': + case 'Code': return 'lorem.sentence'; // MinKey and MaxKey - case 'minkey': + case 'MinKey': return 'number.int'; - case 'maxkey': + case 'MaxKey': return 'number.int'; // Symbol (deprecated) - case 'symbol': + case 'Symbol': return 'lorem.word'; - // DBPointer (deprecated) - case 'dbpointer': + // DBRef + case 'DBRef': return 'database.mongodbObjectId'; // Default fallback