diff --git a/package-lock.json b/package-lock.json index 18f048fe736..1db759b6476 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6032,10 +6032,9 @@ } }, "node_modules/@faker-js/faker": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.0.0.tgz", - "integrity": "sha512-dTDHJSmz6c1OJ6HO7jiUiIb4sB20Dlkb3pxYsKm0qTXm2Bmj97rlXIhlvaFsW2rvCi+OLlwKLVSS6ZxFUVZvjQ==", - "deprecated": "Please update to a newer version", + "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", @@ -58168,9 +58167,9 @@ "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==" }, "@faker-js/faker": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.0.0.tgz", - "integrity": "sha512-dTDHJSmz6c1OJ6HO7jiUiIb4sB20Dlkb3pxYsKm0qTXm2Bmj97rlXIhlvaFsW2rvCi+OLlwKLVSS6ZxFUVZvjQ==" + "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", 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..b7c24288dcf --- /dev/null +++ b/packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.spec.ts @@ -0,0 +1,1258 @@ +import { expect } from 'chai'; +import { faker } from '@faker-js/faker/locale/en'; +import { + generateScript, + type FakerFieldMapping, +} 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); + 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]; + + // 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', () => { + const createFieldMapping = ( + fakerMethod: string, + probability?: number + ): FakerFieldMapping => ({ + mongoType: 'String' as const, + fakerMethod, + fakerArgs: [], + ...(probability !== undefined && { probability }), + }); + + 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) { + 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'); + + // 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('@'); + } + }); + + 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) { + 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); + expect(document.tags[0]).to.be.a('string').and.not.be.empty; + } + }); + + 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) { + // 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); + + // 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('@'); + } + }); + + 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) { + const expectedReturnBlock = `return { + 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'); + } + }); + + 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) { + 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); + + // 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; + } + }); + + 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'), + }; + + const result = generateScript(schema, { + databaseName: 'library', + collectionName: 'publications', + documentCount: 2, + }); + + 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(), + 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'); + } + }); + + 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) { + const expectedReturnBlock = `return {};`; + expect(result.script).to.contain(expectedReturnBlock); + + // Test that the generated document code is executable + testDocumentCodeExecution(result.script); + } + }); + + it('should handle single field', () => { + const schema = { + value: createFieldMapping('number.int'), + }; + + const result = generateScript(schema, { + databaseName: 'testdb', + collectionName: 'coll', + documentCount: 1, + }); + + expect(result.success).to.equal(true); + if (result.success) { + const expectedReturnBlock = `return { + 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'); + } + }); + + 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 + 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'); + } + }); + + 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 + 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'); + } + }); + + 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', () => { + 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) { + 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); + } + }); + + 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) { + 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); + } + }); + + 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: { + length: 5, + elements: { + tags: [4], + }, + }, + }, + }); + + expect(result.success).to.equal(true); + if (result.success) { + const expectedReturnBlock = `return { + users: Array.from({length: 5}, () => ({ + tags: Array.from({length: 4}, () => faker.lorem.word()) + })) + };`; + 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; + } + }); + + 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) 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); + } + }); + + it('should handle multi-dimensional arrays with custom lengths', () => { + const schema = { + 'matrix[][]': createFieldMapping('number.int'), + 'cube[][][]': createFieldMapping('number.float'), + }; + + const result = generateScript(schema, { + databaseName: 'testdb', + collectionName: 'data', + documentCount: 1, + arrayLengthMap: { + matrix: [2, 5], // 2x5 matrix + cube: [3, 4, 2], // 3x4x2 cube + }, + }); + + expect(result.success).to.equal(true); + if (result.success) { + const expectedReturnBlock = `return { + matrix: Array.from({length: 2}, () => Array.from({length: 5}, () => faker.number.int())), + cube: Array.from({length: 3}, () => Array.from({length: 4}, () => Array.from({length: 2}, () => faker.number.float()))) + };`; + expect(result.script).to.contain(expectedReturnBlock); + + // Test that the generated document code is executable + testDocumentCodeExecution(result.script); + } + }); + + 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) { + // 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); + } + }); + }); + + describe('Unrecognized Field Defaults', () => { + it('should use default faker method for unrecognized string fields', () => { + const schema = { + unknownField: { + mongoType: 'String' as const, + fakerMethod: 'unrecognized', + fakerArgs: [], + }, + }; + + const result = generateScript(schema, { + databaseName: 'testdb', + collectionName: 'test', + documentCount: 1, + }); + + expect(result.success).to.equal(true); + if (result.success) { + 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; + } + }); + + it('should use default faker method for unrecognized number fields', () => { + const schema = { + unknownNumber: { + mongoType: 'Number' as const, + fakerMethod: 'unrecognized', + fakerArgs: [], + }, + unknownInt: { + mongoType: 'Int32' as const, + fakerMethod: 'unrecognized', + fakerArgs: [], + }, + unknownInt32: { + mongoType: 'Int32' as const, + fakerMethod: 'unrecognized', + fakerArgs: [], + }, + unknownInt64: { + mongoType: 'Long' as const, + fakerMethod: 'unrecognized', + fakerArgs: [], + }, + unknownLong: { + mongoType: 'Long' as const, + fakerMethod: 'unrecognized', + fakerArgs: [], + }, + unknownDecimal128: { + mongoType: 'Decimal128' as const, + fakerMethod: 'unrecognized', + fakerArgs: [], + }, + }; + + const result = generateScript(schema, { + databaseName: 'testdb', + collectionName: 'test', + documentCount: 1, + }); + + expect(result.success).to.equal(true); + if (result.success) { + // 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'); + } + }); + + it('should use default faker method for unrecognized date fields', () => { + const schema = { + unknownDate: { + mongoType: 'Date' as const, + 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 unrecognized boolean fields', () => { + const schema = { + unknownBool: { + mongoType: 'Boolean' as const, + 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()'); + + // Test that the generated document code is executable + testDocumentCodeExecution(result.script); + } + }); + + it('should use default faker method for unrecognized ObjectId fields', () => { + const schema = { + unknownId: { + mongoType: 'ObjectId' as const, + 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()'); + + // 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: { + mongoType: 'String' as const, + 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 timestamp fields', () => { + const schema = { + timestampField: { + mongoType: 'Timestamp' as const, + 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 regex fields', () => { + const schema = { + regexField: { + mongoType: 'RegExp' as const, + 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: 'Code' as const, + 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); + } + }); + }); + + describe('Faker Arguments', () => { + it('should handle string arguments', () => { + const schema = { + name: { + mongoType: 'String' as const, + 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")'); + + // Test that the generated document code is executable + testDocumentCodeExecution(result.script); + } + }); + + it('should handle number arguments', () => { + const schema = { + age: { + mongoType: 'Number' as const, + 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)'); + + // Test that the generated document code is executable + testDocumentCodeExecution(result.script); + } + }); + + it('should handle JSON object arguments', () => { + const schema = { + score: { + mongoType: 'Number' as const, + 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})' + ); + + // Test that the generated document code is executable + testDocumentCodeExecution(result.script); + } + }); + + it('should handle JSON array arguments', () => { + const schema = { + color: { + mongoType: 'String' as const, + 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'])" + ); + + // Test that the generated document code is executable + testDocumentCodeExecution(result.script); + } + }); + + it('should handle mixed argument types', () => { + const schema = { + description: { + mongoType: 'String' as const, + 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)'); + + // Test that the generated document code is executable + testDocumentCodeExecution(result.script); + } + }); + + it('should safely handle quotes and special characters in string arguments', () => { + const schema = { + quote: { + mongoType: 'String' as const, + fakerMethod: 'helpers.arrayElement', + fakerArgs: [ + { json: '["It\'s a \'test\' string", "another option"]' }, + ], + }, + }; + + 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.helpers.arrayElement(["It\'s a \'test\' string", "another option"])' + ); + + // Test that the generated document code is executable + testDocumentCodeExecution(result.script); + } + }); + + it('should handle empty arguments array', () => { + const schema = { + id: { + mongoType: 'String' as const, + 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()'); + + // Test that the generated document code is executable + testDocumentCodeExecution(result.script); + } + }); + }); + + 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()'); + + // Test that the generated document code is executable + testDocumentCodeExecution(result.script); + } + }); + + 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()'); + + // Test that the generated document code is executable + testDocumentCodeExecution(result.script); + } + }); + + it('should default invalid probability to 1.0', () => { + const schema = { + field1: { + mongoType: 'String' as const, + fakerMethod: 'lorem.word', + fakerArgs: [], + probability: 1.5, // Invalid - should default to 1.0 + }, + field2: { + mongoType: 'String' as const, + fakerMethod: 'lorem.word', + fakerArgs: [], + probability: -0.5, // Invalid - should default to 1.0 + }, + field3: { + mongoType: 'String' as const, + fakerMethod: 'lorem.word', + fakerArgs: [], + probability: 'invalid' as any, // Invalid - should default to 1.0 + }, + }; + + const result = generateScript(schema, { + 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) + 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); + } + }); + + 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) { + 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); + } + }); + + 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) { + 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:' + ); + expect(result.script).not.to.contain( + 'Math.random() < 1 ? { defaultProbability:' + ); + + // Test that the generated document code is executable + testDocumentCodeExecution(result.script); + } + }); + + it('should handle probability with faker arguments', () => { + const schema = { + conditionalAge: { + mongoType: 'Number' as const, + 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) } : {})' + ); + + // Test that the generated document code is executable + testDocumentCodeExecution(result.script); + } + }); + + it('should handle probability with unrecognized fields', () => { + const schema = { + unknownField: { + mongoType: 'String' as const, + 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() } : {})' + ); + + // 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 new file mode 100644 index 00000000000..1fcc7b09f8f --- /dev/null +++ b/packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.ts @@ -0,0 +1,593 @@ +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: MongoDBFieldType; + fakerMethod: string; + fakerArgs: FakerArg[]; + probability?: number; // 0.0 - 1.0 frequency of field (defaults to 1.0) +} + +// Array length configuration for different array types +export type ArrayLengthMap = { + [fieldName: string]: + | 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; + collectionName: string; + arrayLengthMap?: ArrayLengthMap; +} + +export type ScriptResult = + | { script: string; success: true } + | { error: string; success: false }; + +type DocumentStructure = { + [fieldName: string]: + | FakerFieldMapping // Leaf: actual data field + | DocumentStructure // Object: nested fields + | ArrayStructure; // Array: repeated elements +}; + +interface ArrayStructure { + type: 'array'; + elementType: FakerFieldMapping | DocumentStructure | ArrayStructure; +} + +/** + * Entry point method: Generate the final script + */ +export function generateScript( + schema: Record, + options: ScriptOptions +): ScriptResult { + try { + const structure = buildDocumentStructure(schema); + + const documentCode = renderDocumentCode( + structure, + INDENT_SIZE * 2, // 4 spaces: 2 for function body + 2 for inside return statement + options.arrayLengthMap + ); + + const script = `// Mock Data Generator Script +// Generated for collection: ${JSON.stringify( + options.databaseName + )}.${JSON.stringify(options.collectionName)} +// Document count: ${options.documentCount} + +const { faker } = require('@faker-js/faker'); + +// Connect to database +use(${JSON.stringify(options.databaseName)}); + +// 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(${JSON.stringify( + options.collectionName + )}).insertMany(documents); + +console.log(\`Successfully inserted \${documents.length} documents into ${JSON.stringify( + options.databaseName + )}.${JSON.stringify(options.collectionName)}\`);`; + + return { + script, + success: true, + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + }; + } +} + +/** + * 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 (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] === ']') { + // 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; + } + } else { + current += char; + } + } + + if (current) { + parts.push(current); + } + + if (parts.length === 0) { + throw new Error( + `Invalid field path "${fieldPath}": no valid field names found` + ); + } + + 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: FakerFieldMapping +): void { + if (pathParts.length === 0) { + 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 === '[]') { + throw new Error( + 'Invalid field path: array notation "[]" cannot be used without a field name' + ); + } + 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 JavaScript object code from document structure + */ +function renderDocumentCode( + structure: DocumentStructure, + indent: number = INDENT_SIZE, + arrayLengthMap: ArrayLengthMap = {} +): string { + // For each field in structure: + // - If FakerFieldMapping: generate faker call + // - If DocumentStructure: generate nested object + // - If ArrayStructure: generate array + + const fieldIndent = ' '.repeat(indent); + const closingBraceIndent = ' '.repeat(indent - INDENT_SIZE); + const documentFields: string[] = []; + + for (const [fieldName, value] of Object.entries(structure)) { + if ('mongoType' in value) { + // It's a field mapping + const mapping = value as FakerFieldMapping; + const fakerCall = generateFakerCall(mapping); + // 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 + documentFields.push( + `${fieldIndent}...(Math.random() < ${probability} ? { ${formatFieldName( + fieldName + )}: ${fakerCall} } : {})` + ); + } else { + // Normal field inclusion + documentFields.push( + `${fieldIndent}${formatFieldName(fieldName)}: ${fakerCall}` + ); + } + } else if ('type' in value && value.type === 'array') { + // It's an array + const arrayCode = renderArrayCode( + value as ArrayStructure, + indent + INDENT_SIZE, + fieldName, + arrayLengthMap, + 0 // Start at dimension 0 + ); + documentFields.push( + `${fieldIndent}${formatFieldName(fieldName)}: ${arrayCode}` + ); + } else { + // It's a nested object: recursive call + + // Get nested array length map for this field, + // including type validation and fallback for malformed maps + const arrayInfo = arrayLengthMap[fieldName]; + const nestedArrayLengthMap = + arrayInfo && !Array.isArray(arrayInfo) && 'elements' in arrayInfo + ? arrayInfo.elements + : {}; + + const nestedCode = renderDocumentCode( + value as DocumentStructure, + indent + INDENT_SIZE, + nestedArrayLengthMap + ); + documentFields.push( + `${fieldIndent}${formatFieldName(fieldName)}: ${nestedCode}` + ); + } + } + + // Handle empty objects + if (documentFields.length === 0) { + return '{}'; + } + + 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 + */ +function renderArrayCode( + arrayStructure: ArrayStructure, + indent: number = INDENT_SIZE, + fieldName: string = '', + arrayLengthMap: ArrayLengthMap = {}, + dimensionIndex: number = 0 +): string { + const elementType = arrayStructure.elementType; + + // Get array length for this dimension + const arrayInfo = arrayLengthMap[fieldName]; + let arrayLength = DEFAULT_ARRAY_LENGTH; + + if (Array.isArray(arrayInfo)) { + // single or multi-dimensional array: eg. [2, 3, 4] or [6] + arrayLength = arrayInfo[dimensionIndex] ?? DEFAULT_ARRAY_LENGTH; // Fallback for malformed array map + } else if (arrayInfo && 'length' in arrayInfo) { + // Array of objects/documents + arrayLength = arrayInfo.length ?? DEFAULT_ARRAY_LENGTH; + } + + if ('mongoType' in elementType) { + // Array of primitives + 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 + const nestedArrayCode = renderArrayCode( + elementType as ArrayStructure, + indent, + fieldName, + arrayLengthMap, + dimensionIndex + 1 // Next dimension + ); + return `Array.from({length: ${arrayLength}}, () => ${nestedArrayCode})`; + } else { + // Array of objects + const nestedArrayLengthMap = + arrayInfo && !Array.isArray(arrayInfo) && 'elements' in arrayInfo + ? arrayInfo.elements + : {}; // Fallback to empty map for malformed array map + const objectCode = renderDocumentCode( + elementType as DocumentStructure, + indent, + nestedArrayLengthMap + ); + return `Array.from({length: ${arrayLength}}, () => (${objectCode}))`; + } +} + +/** + * Generate faker.js call from field mapping + */ +function generateFakerCall(mapping: FakerFieldMapping): string { + const method = + mapping.fakerMethod === 'unrecognized' + ? getDefaultFakerMethod(mapping.mongoType) + : mapping.fakerMethod; + + const args = formatFakerArgs(mapping.fakerArgs); + return `faker.${method}(${args})`; +} + +/** + * Gets default faker method for unrecognized fields based on MongoDB type + */ +export function getDefaultFakerMethod(mongoType: MongoDBFieldType): string { + switch (mongoType) { + // String types + case 'String': + return 'lorem.word'; + + // Numeric types + case 'Number': + case 'Int32': + case 'Long': + return 'number.int'; + 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': + return 'datatype.boolean'; + + // Binary + case 'Binary': + return 'string.hexadecimal'; + + // Regular expression + case 'RegExp': + return 'lorem.word'; + + // JavaScript code + 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'; + + // DBRef + case 'DBRef': + return 'database.mongodbObjectId'; + + // Default fallback + default: + return 'lorem.word'; + } +} + +/** + * 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[] = []; + + for (let i = 0; i < fakerArgs.length; i++) { + const arg = fakerArgs[i]; + + if (typeof arg === 'string') { + stringifiedArgs.push(JSON.stringify(arg)); + } 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}` + ); + } + } + + return stringifiedArgs.join(', '); +} diff --git a/packages/compass-collection/src/modules/collection-tab.ts b/packages/compass-collection/src/modules/collection-tab.ts index 449e4b503ab..a700b193b70 100644 --- a/packages/compass-collection/src/modules/collection-tab.ts +++ b/packages/compass-collection/src/modules/collection-tab.ts @@ -43,7 +43,6 @@ import type { MockDataGeneratorState, } from '../components/mock-data-generator-modal/types'; -// @ts-expect-error TypeScript warns us about importing ESM module from CommonJS module, but we can ignore since this code will be consumed by webpack. import { faker } from '@faker-js/faker/locale/en'; const DEFAULT_SAMPLE_SIZE = 100;