diff --git a/package-lock.json b/package-lock.json index 6cb305f884f..6ad48eb9e5c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6031,6 +6031,23 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "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", + "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 +47778,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 +58098,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.0.0", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.0.0.tgz", + "integrity": "sha512-dTDHJSmz6c1OJ6HO7jiUiIb4sB20Dlkb3pxYsKm0qTXm2Bmj97rlXIhlvaFsW2rvCi+OLlwKLVSS6ZxFUVZvjQ==" + }, "@fast-csv/parse": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/@fast-csv/parse/-/parse-5.0.5.tgz", @@ -61347,6 +61370,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", diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/faker-mapping-selector.tsx b/packages/compass-collection/src/components/mock-data-generator-modal/faker-mapping-selector.tsx new file mode 100644 index 00000000000..9d64a77e369 --- /dev/null +++ b/packages/compass-collection/src/components/mock-data-generator-modal/faker-mapping-selector.tsx @@ -0,0 +1,79 @@ +import { + Banner, + BannerVariant, + Body, + css, + Option, + palette, + Select, + spacing, +} from '@mongodb-js/compass-components'; +import React from 'react'; +import { UNRECOGNIZED_FAKER_METHOD } from '../../modules/collection-tab'; + +const fieldMappingSelectorsStyles = css({ + width: '50%', + display: 'flex', + flexDirection: 'column', + gap: spacing[200], +}); + +const labelStyles = css({ + color: palette.gray.dark1, + fontWeight: 600, +}); + +interface Props { + activeJsonType: string; + activeFakerFunction: string; + onJsonTypeSelect: (jsonType: string) => void; + onFakerFunctionSelect: (fakerFunction: string) => void; +} + +const FakerMappingSelector = ({ + activeJsonType, + activeFakerFunction, + onJsonTypeSelect, + onFakerFunctionSelect, +}: Props) => { + return ( +
+ Mapping + + + {activeFakerFunction === UNRECOGNIZED_FAKER_METHOD && ( + + Please select a function or we will default fill this field with the + string "Unrecognized" + + )} + {/* TODO(CLOUDP-344400): Render faker function parameters once we have a way to validate them. */} +
+ ); +}; + +export default FakerMappingSelector; diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/faker-schema-editor-screen.tsx b/packages/compass-collection/src/components/mock-data-generator-modal/faker-schema-editor-screen.tsx index c888cafdcc7..9629d0a251e 100644 --- a/packages/compass-collection/src/components/mock-data-generator-modal/faker-schema-editor-screen.tsx +++ b/packages/compass-collection/src/components/mock-data-generator-modal/faker-schema-editor-screen.tsx @@ -1,10 +1,172 @@ +import { + Body, + Button, + ButtonSize, + ButtonVariant, + css, + Link, + palette, + spacing, + SpinLoaderWithLabel, +} from '@mongodb-js/compass-components'; import React from 'react'; +import FieldSelector from './schema-field-selector'; +import FakerMappingSelector from './faker-mapping-selector'; +import type { FakerSchemaMapping, MockDataGeneratorState } from './types'; -// TODO: More to come from CLOUDP-333853, CLOUDP-333854 -const FakerSchemaEditorScreen = () => { +const containerStyles = css({ + display: 'flex', + flexDirection: 'column', + gap: spacing[400], +}); + +const innerEditorStyles = css({ + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', +}); + +const titleStyles = css({ + color: palette.black, + fontWeight: 600, + fontSize: '16px', + lineHeight: '20px', + marginBottom: 0, +}); + +const bodyStyles = css({ + color: palette.gray.dark1, +}); + +const confirmMappingsButtonStyles = css({ + width: '200px', +}); + +const schemaEditorLoaderStyles = css({ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', +}); + +const FakerSchemaEditorContent = ({ + fakerSchemaMappings, + onSchemaConfirmed, +}: { + fakerSchemaMappings: FakerSchemaMapping[]; + onSchemaConfirmed: (isConfirmed: boolean) => void; +}) => { + const [fakerSchemaFormValues, setFakerSchemaFormValues] = + React.useState>(fakerSchemaMappings); + const [activeField, setActiveField] = React.useState( + fakerSchemaFormValues[0].fieldPath + ); + + const activeJsonType = fakerSchemaFormValues.find( + (mapping) => mapping.fieldPath === activeField + )?.mongoType; + const activeFakerFunction = fakerSchemaFormValues.find( + (mapping) => mapping.fieldPath === activeField + )?.fakerMethod; + + const resetIsSchemaConfirmed = () => { + onSchemaConfirmed(false); + }; + + const onJsonTypeSelect = (newJsonType: string) => { + const updatedFakerFieldMapping = fakerSchemaFormValues.find( + (mapping) => mapping.fieldPath === activeField + ); + if (updatedFakerFieldMapping) { + updatedFakerFieldMapping.mongoType = newJsonType; + setFakerSchemaFormValues( + fakerSchemaFormValues.map((mapping) => + mapping.fieldPath === activeField ? updatedFakerFieldMapping : mapping + ) + ); + resetIsSchemaConfirmed(); + } + }; + + const onFakerFunctionSelect = (newFakerFunction: string) => { + const updatedFakerFieldMapping = fakerSchemaFormValues.find( + (mapping) => mapping.fieldPath === activeField + ); + if (updatedFakerFieldMapping) { + updatedFakerFieldMapping.fakerMethod = newFakerFunction; + setFakerSchemaFormValues( + fakerSchemaFormValues.map((mapping) => + mapping.fieldPath === activeField ? updatedFakerFieldMapping : mapping + ) + ); + resetIsSchemaConfirmed(); + } + }; + + return ( + <> +
+ mapping.fieldPath)} + onFieldSelect={setActiveField} + /> + {activeJsonType && activeFakerFunction && ( + + )} +
+ + + ); +}; + +const FakerSchemaEditorScreen = ({ + onSchemaConfirmed, + fakerSchemaGenerationState, +}: { + isSchemaConfirmed: boolean; + onSchemaConfirmed: (isConfirmed: boolean) => void; + fakerSchemaGenerationState: MockDataGeneratorState; +}) => { return ( -
- Schema Editor Content Placeholder +
+
+

+ Confirm Field to Faker Function Mappings +

+ + We have sampled your collection and created a schema based on your + documents. That schema has been sent to an LLM and it has returned the + following mapping between your schema fields and{' '} + faker functions + . + +
+ {fakerSchemaGenerationState.status === 'in-progress' && ( +
+ +
+ )} + {fakerSchemaGenerationState.status === 'completed' && ( + + )}
); }; diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.spec.tsx b/packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.spec.tsx index 7fec9d737fd..2e313aaaefc 100644 --- a/packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.spec.tsx +++ b/packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.spec.tsx @@ -15,6 +15,7 @@ import { StepButtonLabelMap } from './constants'; import type { CollectionState } from '../../modules/collection-tab'; import { default as collectionTabReducer } from '../../modules/collection-tab'; import type { ConnectionInfo } from '@mongodb-js/connection-info'; +import type { MockDataSchemaResponse } from '@mongodb-js/compass-generative-ai'; describe('MockDataGeneratorModal', () => { async function renderModal({ @@ -80,10 +81,19 @@ describe('MockDataGeneratorModal', () => { atlasAiService: { getMockDataSchema: () => { return Promise.resolve({ - contents: { - fields: [], + content: { + fields: [ + { + fieldPath: 'name', + mongoType: 'string', + fakerMethod: 'person.firstName', + fakerArgs: [], + isArray: false, + probability: 1.0, + }, + ], }, - }); + } as MockDataSchemaResponse); }, }, workspaces: {}, @@ -175,7 +185,7 @@ describe('MockDataGeneratorModal', () => { userEvent.click(screen.getByText('Confirm')); await waitFor(() => { - expect(screen.getByTestId('faker-schema-editor')).to.exist; + expect(screen.getByTestId('faker-schema-editor-loader')).to.exist; }); userEvent.click(screen.getByText('Cancel')); @@ -191,7 +201,7 @@ describe('MockDataGeneratorModal', () => { userEvent.click(screen.getByText('Confirm')); await waitFor(() => { - expect(screen.getByTestId('faker-schema-editor')).to.exist; + expect(screen.getByTestId('faker-schema-editor-loader')).to.exist; }); userEvent.click(screen.getByText('Back')); @@ -273,6 +283,214 @@ describe('MockDataGeneratorModal', () => { }); }); + describe('on the schema editor step', () => { + const mockServicesWithMockDataResponse = createMockServices(); + mockServicesWithMockDataResponse.atlasAiService.getMockDataSchema = () => + Promise.resolve({ + content: { + fields: [ + { + fieldPath: 'name', + mongoType: 'string', + fakerMethod: 'person.firstName', + fakerArgs: [], + isArray: false, + probability: 1.0, + }, + { + fieldPath: 'age', + mongoType: 'int', + fakerMethod: 'number.int', + fakerArgs: [], + isArray: false, + probability: 1.0, + }, + { + fieldPath: 'email', + mongoType: 'string', + fakerMethod: 'internet', + fakerArgs: [], + isArray: false, + probability: 1.0, + }, + { + fieldPath: 'username', + mongoType: 'string', + fakerMethod: 'noSuchMethod', + fakerArgs: [], + isArray: false, + probability: 1.0, + }, + ], + }, + }); + + it('shows a loading spinner when the faker schema generation is in progress', async () => { + const mockServices = createMockServices(); + mockServices.atlasAiService.getMockDataSchema = () => + new Promise((resolve) => + setTimeout( + () => + resolve({ + content: { + fields: [], + }, + }), + 1000 + ) + ); + + await renderModal(); + + // advance to the schema editor step + userEvent.click(screen.getByText('Confirm')); + expect(screen.getByTestId('faker-schema-editor-loader')).to.exist; + }); + + it('shows the faker schema editor when the faker schema generation is completed', async () => { + await renderModal({ mockServices: mockServicesWithMockDataResponse }); + + // advance to the schema editor step + userEvent.click(screen.getByText('Confirm')); + + expect(await screen.findByTestId('faker-schema-editor')).to.exist; + expect(screen.getByText('name')).to.exist; + expect(screen.getByText('age')).to.exist; + }); + + it('shows correct values for the faker schema editor', async () => { + await renderModal({ mockServices: mockServicesWithMockDataResponse }); + + // advance to the schema editor step + userEvent.click(screen.getByText('Confirm')); + await waitFor(() => { + expect(screen.getByTestId('faker-schema-editor')).to.exist; + }); + // the "name" field should be selected by default + expect(screen.getByText('name')).to.exist; + expect(screen.getByLabelText('JSON Type')).to.have.value('string'); + expect(screen.getByLabelText('Faker Function')).to.have.value( + 'person.firstName' + ); + // select the "age" field + userEvent.click(screen.getByText('age')); + expect(screen.getByText('age')).to.exist; + expect(screen.getByLabelText('JSON Type')).to.have.value('int'); + expect(screen.getByLabelText('Faker Function')).to.have.value( + 'number.int' + ); + // select the "email" field + userEvent.click(screen.getByText('email')); + expect(screen.getByText('email')).to.exist; + expect(screen.getByLabelText('JSON Type')).to.have.value('string'); + // the "email" field should have a warning banner since the faker method is invalid + expect(screen.getByLabelText('Faker Function')).to.have.value( + 'Unrecognized' + ); + expect( + screen.getByText( + 'Please select a function or we will default fill this field with the string "Unrecognized"' + ) + ).to.exist; + + // select the "username" field + userEvent.click(screen.getByText('username')); + expect(screen.getByText('username')).to.exist; + expect(screen.getByLabelText('JSON Type')).to.have.value('string'); + expect(screen.getByLabelText('Faker Function')).to.have.value( + 'Unrecognized' + ); + }); + + it('disables the Next button when the faker schema mapping is not confirmed', async () => { + await renderModal({ + mockServices: mockServicesWithMockDataResponse, + }); + + // advance to the schema editor step + userEvent.click(screen.getByText('Confirm')); + await waitFor(() => { + expect(screen.getByTestId('faker-schema-editor')).to.exist; + }); + + expect( + screen.getByTestId('next-step-button').getAttribute('aria-disabled') + ).to.equal('true'); + }); + + it('resets the confirm schema mapping state when the user clicks the back button then goes back to the schema editor step', async () => { + await renderModal({ + mockServices: mockServicesWithMockDataResponse, + }); + + // advance to the schema editor step + userEvent.click(screen.getByText('Confirm')); + await waitFor(() => { + expect(screen.getByTestId('faker-schema-editor')).to.exist; + }); + expect( + screen.getByTestId('next-step-button').getAttribute('aria-disabled') + ).to.equal('true'); + // click confirm mappings button + userEvent.click(screen.getByText('Confirm mappings')); + expect( + screen.getByTestId('next-step-button').getAttribute('aria-disabled') + ).to.equal('false'); + + // click back button + userEvent.click(screen.getByText('Back')); + await waitFor(() => { + expect(screen.getByTestId('raw-schema-confirmation')).to.exist; + }); + + // click next button to advance to the schema editor step again + userEvent.click(screen.getByTestId('next-step-button')); + await waitFor(() => { + expect(screen.getByTestId('faker-schema-editor')).to.exist; + }); + // the next button should be disabled again + expect( + screen.getByTestId('next-step-button').getAttribute('aria-disabled') + ).to.equal('true'); + }); + + it('preserves the confirm schema mapping state when the user clicks the next button then goes back to the schema editor step', async () => { + await renderModal({ + mockServices: mockServicesWithMockDataResponse, + }); + + // advance to the schema editor step + userEvent.click(screen.getByText('Confirm')); + await waitFor(() => { + expect(screen.getByTestId('faker-schema-editor')).to.exist; + }); + expect( + screen.getByTestId('next-step-button').getAttribute('aria-disabled') + ).to.equal('true'); + // click confirm mappings button + userEvent.click(screen.getByText('Confirm mappings')); + expect( + screen.getByTestId('next-step-button').getAttribute('aria-disabled') + ).to.equal('false'); + + // click next button + userEvent.click(screen.getByTestId('next-step-button')); + await waitFor(() => { + expect(screen.queryByTestId('faker-schema-editor')).to.not.exist; + }); + + // click back button to go back to the schema editor step + userEvent.click(screen.getByText('Back')); + await waitFor(() => { + expect(screen.getByTestId('faker-schema-editor')).to.exist; + }); + // the next button should not be disabled + expect( + screen.getByTestId('next-step-button').getAttribute('aria-disabled') + ).to.equal('false'); + }); + }); + describe('on the generate data step', () => { it('enables the Back button', async () => { await renderModal({ currentStep: MockDataGeneratorStep.GENERATE_DATA }); diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.tsx b/packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.tsx index 5f6982de0cb..e2aa91a67a8 100644 --- a/packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.tsx +++ b/packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.tsx @@ -13,7 +13,7 @@ import { spacing, } from '@mongodb-js/compass-components'; -import { MockDataGeneratorStep } from './types'; +import { type MockDataGeneratorState, MockDataGeneratorStep } from './types'; import { StepButtonLabelMap } from './constants'; import type { CollectionState } from '../../modules/collection-tab'; import { @@ -50,6 +50,7 @@ interface Props { onConfirmSchema: () => Promise; onPreviousStep: () => void; namespace: string; + fakerSchemaGenerationState: MockDataGeneratorState; } const MockDataGeneratorModal = ({ @@ -60,13 +61,23 @@ const MockDataGeneratorModal = ({ onConfirmSchema, onPreviousStep, namespace, + fakerSchemaGenerationState, }: Props) => { + const [isSchemaConfirmed, setIsSchemaConfirmed] = + React.useState(false); + const modalBodyContent = useMemo(() => { switch (currentStep) { case MockDataGeneratorStep.SCHEMA_CONFIRMATION: return ; case MockDataGeneratorStep.SCHEMA_EDITOR: - return ; + return ( + + ); case MockDataGeneratorStep.DOCUMENT_COUNT: return <>; // TODO: CLOUDP-333856 case MockDataGeneratorStep.PREVIEW_DATA: @@ -74,7 +85,10 @@ const MockDataGeneratorModal = ({ case MockDataGeneratorStep.GENERATE_DATA: return ; } - }, [currentStep]); + }, [currentStep, fakerSchemaGenerationState, isSchemaConfirmed]); + + const isNextButtonDisabled = + currentStep === MockDataGeneratorStep.SCHEMA_EDITOR && !isSchemaConfirmed; const handleNextClick = () => { if (currentStep === MockDataGeneratorStep.GENERATE_DATA) { @@ -89,6 +103,14 @@ const MockDataGeneratorModal = ({ const shouldShowNamespace = currentStep !== MockDataGeneratorStep.GENERATE_DATA; + const handlePreviousClick = () => { + if (currentStep === MockDataGeneratorStep.SCHEMA_EDITOR) { + // reset isSchemaConfirmed state when previous step is clicked + setIsSchemaConfirmed(false); + } + onPreviousStep(); + }; + return ( @@ -135,6 +158,7 @@ const mapStateToProps = (state: CollectionState) => ({ isOpen: state.mockDataGenerator.isModalOpen, currentStep: state.mockDataGenerator.currentStep, namespace: state.namespace, + fakerSchemaGenerationState: state.fakerSchemaGeneration, }); const ConnectedMockDataGeneratorModal = connect(mapStateToProps, { diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/schema-field-selector.tsx b/packages/compass-collection/src/components/mock-data-generator-modal/schema-field-selector.tsx new file mode 100644 index 00000000000..ccfeff58482 --- /dev/null +++ b/packages/compass-collection/src/components/mock-data-generator-modal/schema-field-selector.tsx @@ -0,0 +1,119 @@ +import React from 'react'; +import { + css, + cx, + spacing, + palette, + useDarkMode, + Body, +} from '@mongodb-js/compass-components'; + +const fieldsContainerStyles = css({ + width: '40%', + display: 'flex', + flexDirection: 'column', + gap: spacing[100], +}); + +const fieldSelectorStyles = css({ + maxHeight: '300px', + overflow: 'auto', +}); + +const buttonStyles = css({ + borderRadius: spacing[100], + cursor: 'pointer', + marginBottom: spacing[100], + background: 'none', + border: 'none', + width: '100%', + padding: spacing[200], + textAlign: 'left', + fontWeight: 500, +}); + +const activeStylesLight = css({ + color: palette.green.dark2, + backgroundColor: palette.green.light3, + fontWeight: 600, + + '&:active,&:focus': { + backgroundColor: palette.green.light3, + }, +}); + +const activeStylesDark = css({ + color: palette.white, + '&:active,&:focus': { + backgroundColor: palette.gray.dark3, + color: palette.white, + }, +}); + +const hoverStylesLight = css({ + '&:hover,&:focus': { + backgroundColor: palette.gray.light2, + color: palette.black, + }, +}); + +const hoverStylesDark = css({ + '&:hover,&:focus': { + backgroundColor: palette.gray.dark3, + color: palette.gray.light2, + }, +}); + +const labelStyles = css({ + color: palette.gray.dark1, + fontWeight: 600, +}); + +type SidebarProps = { + activeField: string; + onFieldSelect: (field: string) => void; + fields: Array; +}; + +const FieldSelector: React.FunctionComponent = ({ + activeField, + fields, + onFieldSelect, +}) => { + const darkMode = useDarkMode(); + + return ( +
+ Document Fields +
+ {fields.map((field) => ( + + ))} +
+
+ ); +}; + +export default FieldSelector; diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/types.ts b/packages/compass-collection/src/components/mock-data-generator-modal/types.ts index 5812f3693a4..279bc3f3921 100644 --- a/packages/compass-collection/src/components/mock-data-generator-modal/types.ts +++ b/packages/compass-collection/src/components/mock-data-generator-modal/types.ts @@ -19,7 +19,7 @@ type MockDataGeneratorInProgressState = { type MockDataGeneratorCompletedState = { status: 'completed'; - fakerSchema: MockDataSchemaResponse; + fakerSchema: FakerSchemaMapping[]; requestId: string; }; @@ -34,3 +34,8 @@ export type MockDataGeneratorState = | MockDataGeneratorInProgressState | MockDataGeneratorCompletedState | MockDataGeneratorErrorState; + +export type FakerSchemaMapping = Omit< + MockDataSchemaResponse['content']['fields'][number], + 'isArray' +>; diff --git a/packages/compass-collection/src/modules/collection-tab.ts b/packages/compass-collection/src/modules/collection-tab.ts index 8c7323045f6..449e4b503ab 100644 --- a/packages/compass-collection/src/modules/collection-tab.ts +++ b/packages/compass-collection/src/modules/collection-tab.ts @@ -38,7 +38,13 @@ import { } from '../transform-schema-to-field-info'; import type { Document, MongoError } from 'mongodb'; import { MockDataGeneratorStep } from '../components/mock-data-generator-modal/types'; -import type { MockDataGeneratorState } from '../components/mock-data-generator-modal/types'; +import type { + FakerSchemaMapping, + 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; @@ -52,6 +58,7 @@ function isAction( } const ERROR_CODE_MAX_TIME_MS_EXPIRED = 50; +export const UNRECOGNIZED_FAKER_METHOD = 'Unrecognized'; function getErrorDetails(error: Error): SchemaAnalysisError { if (error instanceof ProcessSchemaUnsupportedStateError) { @@ -178,7 +185,7 @@ export interface FakerMappingGenerationStartedAction { export interface FakerMappingGenerationCompletedAction { type: CollectionActions.FakerMappingGenerationCompleted; - fakerSchema: MockDataSchemaResponse; + fakerSchema: FakerSchemaMapping[]; requestId: string; } @@ -692,6 +699,34 @@ export const cancelSchemaAnalysis = (): CollectionThunkAction => { }; }; +const validateFakerSchema = ( + fakerSchema: MockDataSchemaResponse, + logger: Logger +) => { + return fakerSchema.content.fields.map((field) => { + const { fakerMethod } = field; + + const [moduleName, methodName, ...rest] = fakerMethod.split('.'); + if ( + rest.length > 0 || + typeof (faker as any)[moduleName]?.[methodName] !== 'function' + ) { + logger.log.warn( + mongoLogId(1_001_000_372), + 'Collection', + 'Invalid faker method', + { fakerMethod } + ); + return { + ...field, + fakerMethod: UNRECOGNIZED_FAKER_METHOD, + }; + } + + return field; + }); +}; + export const generateFakerMappings = (): CollectionThunkAction< Promise > => { @@ -758,10 +793,12 @@ export const generateFakerMappings = (): CollectionThunkAction< connectionInfoRef.current ); + const validatedFakerSchema = validateFakerSchema(response, logger); + fakerSchemaGenerationAbortControllerRef.current = undefined; dispatch({ type: CollectionActions.FakerMappingGenerationCompleted, - fakerSchema: response, + fakerSchema: validatedFakerSchema, requestId: requestId, }); } catch (e) { diff --git a/packages/compass-web/webpack.config.js b/packages/compass-web/webpack.config.js index c76ff5cbc92..4302f432967 100644 --- a/packages/compass-web/webpack.config.js +++ b/packages/compass-web/webpack.config.js @@ -325,6 +325,8 @@ module.exports = (env, args) => { // bson is not that big, but is a shared dependency of compass-web, // compass-components and bson-transpilers, so splitting it out 'bson', + // dependency of compass-collection + '@faker-js/faker', ]); return bundles;