Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ import {
palette,
Select,
spacing,
TextInput,
} from '@mongodb-js/compass-components';
import React from 'react';
import { UNRECOGNIZED_FAKER_METHOD } from '../../modules/collection-tab';
import type { MongoDBFieldType } from '@mongodb-js/compass-generative-ai';
import type { FakerArg } from './script-generation-utils';

const fieldMappingSelectorsStyles = css({
width: '50%',
Expand All @@ -24,16 +26,106 @@ const labelStyles = css({
fontWeight: 600,
});

/**
* Renders read-only TextInput components for each key-value pair in a faker arguments object.
*/
const getFakerArgsInputFromObject = (fakerArgsObject: Record<string, any>) => {
Copy link
Collaborator

@kpamaran kpamaran Sep 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: use render in name to clarify React Elements are returned, something like renderFakerArgsInput

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is no longer applicable since we will show a preview of the faker call.

return Object.entries(fakerArgsObject).map(([key, item]: [string, any]) => {
if (typeof item === 'string' || typeof item === 'boolean') {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't boolean use something like a radio option instead of arbitrary text?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No longer applicable.

return (
<TextInput
key={`faker-arg-${key}`}
type="text"
label={key}
aria-label={`Faker Arg ${key}`}
readOnly
value={item.toString()}
/>
);
} else if (typeof item === 'number') {
return (
<TextInput
key={`faker-arg-${key}`}
type="number"
label={key}
aria-label={`Faker Arg ${key}`}
readOnly
value={item.toString()}
/>
);
} else if (
Array.isArray(item) &&
item.length > 0 &&
typeof item[0] === 'string'
) {
return (
<TextInput
key={`faker-arg-${key}`}
type="text"
label={key}
aria-label={`Faker Arg ${key}`}
readOnly
value={item.join(', ')}
/>
);
}
return null;
});
};

/**
* Renders TextInput components for each faker argument based on its type.
*/
const getFakerArgsInput = (fakerArgs: FakerArg[]) => {
return fakerArgs.map((arg, idx) => {
if (typeof arg === 'string' || typeof arg === 'boolean') {
return (
<TextInput
key={`faker-arg-${idx}`}
type="text"
label="Faker Arg"
Copy link
Collaborator

@kpamaran kpamaran Sep 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

labels should be unique across the form's inputs

required
value={arg.toString()}
/>
);
} else if (typeof arg === 'number') {
return (
<TextInput
key={`faker-arg-${idx}`}
type="number"
label="Faker Arg"
readOnly
value={arg.toString()}
/>
);
} else if (typeof arg === 'object' && 'json' in arg) {
// parse the object
let parsedArg;
try {
parsedArg = JSON.parse(arg.json);
} catch {
// If parsing fails, skip rendering this arg
return null;
}
if (typeof parsedArg === 'object') {
return getFakerArgsInputFromObject(parsedArg);
}
}
});
};

interface Props {
activeJsonType: string;
activeFakerFunction: string;
onJsonTypeSelect: (jsonType: MongoDBFieldType) => void;
activeFakerArgs: FakerArg[];
onFakerFunctionSelect: (fakerFunction: string) => void;
}

const FakerMappingSelector = ({
activeJsonType,
activeFakerFunction,
activeFakerArgs,
onJsonTypeSelect,
onFakerFunctionSelect,
}: Props) => {
Expand Down Expand Up @@ -72,7 +164,7 @@ const FakerMappingSelector = ({
string &quot;Unrecognized&quot;
</Banner>
)}
{/* TODO(CLOUDP-344400): Render faker function parameters once we have a way to validate them. */}
{getFakerArgsInput(activeFakerArgs)}
</div>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ const FakerSchemaEditorContent = ({

const activeJsonType = fakerSchemaFormValues[activeField]?.mongoType;
const activeFakerFunction = fakerSchemaFormValues[activeField]?.fakerMethod;
const activeFakerArgs = fakerSchemaFormValues[activeField]?.fakerArgs;

const resetIsSchemaConfirmed = () => {
onSchemaConfirmed(false);
Expand Down Expand Up @@ -109,6 +110,7 @@ const FakerSchemaEditorContent = ({
<FakerMappingSelector
activeJsonType={activeJsonType}
activeFakerFunction={activeFakerFunction}
activeFakerArgs={activeFakerArgs}
onJsonTypeSelect={onJsonTypeSelect}
onFakerFunctionSelect={onFakerFunctionSelect}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ describe('MockDataGeneratorModal', () => {
},
};

// @ts-expect-error // TODO: fix ts error
const store = createStore(
collectionTabReducer,
initialState,
Expand Down Expand Up @@ -538,6 +539,44 @@ describe('MockDataGeneratorModal', () => {
);
});

it('does not show faker args that are too large', async () => {
const largeLengthArgs = Array.from({ length: 11 }, () => 'test');
const mockServices = createMockServices();
mockServices.atlasAiService.getMockDataSchema = () =>
Promise.resolve({
fields: [
{
fieldPath: 'name',
mongoType: 'String',
fakerMethod: 'person.firstName',
fakerArgs: [JSON.stringify(largeLengthArgs)],
isArray: false,
probability: 1.0,
},
],
});

await renderModal({
mockServices,
schemaAnalysis: {
...defaultSchemaAnalysisState,
processedSchema: {
name: {
type: 'String',
probability: 1.0,
},
},
sampleDocument: { name: 'Peaches' },
},
});

// advance to the schema editor step
userEvent.click(screen.getByText('Confirm'));
await waitFor(() => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems to be an assertion missing? I'm not connecting how "faker args that are too large" are not shown

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, I added the assertion in utils.spec.ts. Fixing.

expect(screen.getByTestId('faker-schema-editor')).to.exist;
});
});

it('disables the Next button when the faker schema mapping is not confirmed', async () => {
await renderModal({
mockServices: mockServicesWithMockDataResponse,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import type { MongoDBFieldType } from '@mongodb-js/compass-generative-ai';
import type { FakerFieldMapping } from './types';

export type FakerArg = string | number | boolean | { json: string };
export type FakerArg =
| string
| number
| boolean
| { json: string }
| FakerArg[];

const DEFAULT_ARRAY_LENGTH = 3;
const INDENT_SIZE = 2;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import { expect } from 'chai';
import { areFakerArgsValid, isValidFakerMethod } from './utils';

describe('Mock Data Generator Utils', function () {
describe('areFakerArgsValid', () => {
it('returns true for empty array', () => {
expect(areFakerArgsValid([])).to.be.true;
});

it('returns false if array length exceeds max faker args length', () => {
const arr = Array(11).fill(1);
expect(areFakerArgsValid(arr)).to.be.false;
});

it('returns true for valid numbers, strings, booleans', () => {
expect(areFakerArgsValid([1, 'foo', true, false, 0])).to.be.true;
});

it('returns false for non-finite numbers', () => {
expect(areFakerArgsValid([Infinity])).to.be.false;
expect(areFakerArgsValid([NaN])).to.be.false;
});

it('returns false for strings exceeding max faker string length', () => {
const longStr = 'a'.repeat(1001);
expect(areFakerArgsValid([longStr])).to.be.false;
});

it('returns true for nested valid arrays', () => {
expect(
areFakerArgsValid([
[1, 'foo', true],
[2, false],
])
).to.be.true;
});

it('returns false for nested arrays exceeding max faker args length', () => {
const nested = [Array(11).fill(1)];
expect(areFakerArgsValid(nested)).to.be.false;
});

it('returns true for valid object with json property', () => {
const obj = { json: JSON.stringify({ a: 1, b: 'foo', c: true }) };
expect(areFakerArgsValid([obj])).to.be.true;
});

it('returns false for object with invalid json property', () => {
const obj = { json: '{invalid json}' };
expect(areFakerArgsValid([obj])).to.be.false;
});

it('returns false for object with json property containing invalid values', () => {
const obj = { json: JSON.stringify({ a: Infinity }) };
expect(areFakerArgsValid([obj])).to.be.false;
});

it('returns false for unrecognized argument types', () => {
expect(areFakerArgsValid([undefined as any])).to.be.false;
expect(areFakerArgsValid([null as any])).to.be.false;
expect(areFakerArgsValid([(() => {}) as any])).to.be.false;
});

it('returns true for deeply nested valid structures', () => {
const obj = {
json: JSON.stringify({
a: [1, { json: JSON.stringify({ b: 'foo' }) }],
}),
};
expect(areFakerArgsValid([obj])).to.be.true;
});

it('returns false for deeply nested invalid structures', () => {
const obj = {
json: JSON.stringify({
a: [1, { json: JSON.stringify({ b: Infinity }) }],
}),
};
expect(areFakerArgsValid([obj])).to.be.false;
});

describe('isValidFakerMethod', () => {
it('returns false for invalid method format', () => {
expect(isValidFakerMethod('invalidMethod', [])).to.deep.equal({
isValid: false,
fakerArgs: [],
});
expect(isValidFakerMethod('internet.email.extra', [])).to.deep.equal({
isValid: false,
fakerArgs: [],
});
});

it('returns false for non-existent faker module', () => {
expect(isValidFakerMethod('notamodule.email', [])).to.deep.equal({
isValid: false,
fakerArgs: [],
});
});

it('returns false for non-existent faker method', () => {
expect(isValidFakerMethod('internet.notamethod', [])).to.deep.equal({
isValid: false,
fakerArgs: [],
});
});

it('returns true for valid method without arguments', () => {
const result = isValidFakerMethod('internet.email', []);
expect(result.isValid).to.be.true;
expect(result.fakerArgs).to.deep.equal([]);
});

it('returns true for valid method with valid arguments', () => {
// name.firstName takes optional gender argument
const result = isValidFakerMethod('name.firstName', ['female']);
expect(result.isValid).to.be.true;
expect(result.fakerArgs).to.deep.equal(['female']);
});

it('returns true for valid method with no args if args are invalid but fallback works', () => {
// internet.email does not take arguments, so passing one should fallback to []
const result = isValidFakerMethod('internet.email', []);
expect(result.isValid).to.be.true;
expect(result.fakerArgs).to.deep.equal([]);
});

it('returns false for valid method with invalid arguments and fallback fails', () => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be invalid arguments with valid method falls back to true and strips args? That's what's happening I think, which is why this test is failing right now

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, forgot to update it.

// date.month expects at most one argument, passing an object will fail both attempts
const result = isValidFakerMethod('date.month', [
{ foo: 'bar' } as any,
]);
expect(result.isValid).to.be.false;
expect(result.fakerArgs).to.deep.equal([]);
});

it('returns false for helpers methods except arrayElement', () => {
expect(isValidFakerMethod('helpers.fake', [])).to.deep.equal({
isValid: false,
fakerArgs: [],
});
expect(isValidFakerMethod('helpers.slugify', [])).to.deep.equal({
isValid: false,
fakerArgs: [],
});
});

it('returns true for helpers.arrayElement with valid arguments', () => {
const arr = ['a', 'b', 'c'];
const result = isValidFakerMethod('helpers.arrayElement', [arr]);
expect(result.isValid).to.be.true;
expect(result.fakerArgs).to.deep.equal([arr]);
});

it('returns false for helpers.arrayElement with invalid arguments', () => {
// Exceeding max args length
const arr = Array(11).fill('x');
const result = isValidFakerMethod('helpers.arrayElement', [arr]);
expect(result.isValid).to.be.false;
expect(result.fakerArgs).to.deep.equal([]);
});

it('returns false for method with invalid fakerArgs', () => {
Copy link
Collaborator

@jcobis jcobis Sep 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My comment applies here too I believe. Both tests should expect isValid: true and fakerArgs: []

// Passing Infinity as argument
const result = isValidFakerMethod('name.firstName', [Infinity]);
expect(result.isValid).to.be.false;
expect(result.fakerArgs).to.deep.equal([]);
});
});
});
});
Loading
Loading