Schema Editor Content Placeholder
@@ -9,4 +9,4 @@ const FakerSchemaEditor = () => {
);
};
-export default FakerSchemaEditor;
+export default FakerSchemaEditorScreen;
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 e45f5816cf5..7fec9d737fd 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
@@ -20,10 +20,12 @@ describe('MockDataGeneratorModal', () => {
async function renderModal({
isOpen = true,
currentStep = MockDataGeneratorStep.SCHEMA_CONFIRMATION,
+ enableGenAISampleDocumentPassing = false,
mockServices = createMockServices(),
connectionInfo,
}: {
isOpen?: boolean;
+ enableGenAISampleDocumentPassing?: boolean;
currentStep?: MockDataGeneratorStep;
mockServices?: any;
connectionInfo?: ConnectionInfo;
@@ -63,7 +65,12 @@ describe('MockDataGeneratorModal', () => {
,
- connectionInfo
+ connectionInfo,
+ {
+ preferences: {
+ enableGenAISampleDocumentPassing,
+ },
+ }
);
}
@@ -204,6 +211,37 @@ describe('MockDataGeneratorModal', () => {
).to.equal('true');
});
+ it('displays the namespace', async () => {
+ await renderModal();
+ expect(screen.getByText('test.collection')).to.exist;
+ });
+
+ it('uses the appropriate copy when Generative AI sample document passing is enabled', async () => {
+ await renderModal({ enableGenAISampleDocumentPassing: true });
+ expect(screen.getByText('Sample Documents Collected')).to.exist;
+ expect(
+ screen.getByText(
+ 'A sample of documents from your collection will be sent to an LLM for processing.'
+ )
+ ).to.exist;
+ // fragment from { "name": "John" }
+ expect(screen.getByText('"John"')).to.exist;
+ expect(screen.queryByText('"String"')).to.not.exist;
+ });
+
+ it('uses the appropriate copy when Generative AI sample document passing is disabled', async () => {
+ await renderModal();
+ expect(screen.getByText('Document Schema Identified')).to.exist;
+ expect(
+ screen.queryByText(
+ 'We have identified the following schema from your documents. This schema will be sent to an LLM for processing.'
+ )
+ ).to.exist;
+ // fragment from { "name": "String" }
+ expect(screen.getByText('"String"')).to.exist;
+ expect(screen.queryByText('"John"')).to.not.exist;
+ });
+
it('renders the faker schema editor when the confirm button is clicked', async () => {
await renderModal();
@@ -230,10 +268,9 @@ describe('MockDataGeneratorModal', () => {
expect(screen.queryByTestId('faker-schema-editor')).to.not.exist;
});
- // todo: assert a user-friendly error is displayed (CLOUDP-333852)
+ expect(screen.getByText('LLM Request failed. Please confirm again.')).to
+ .exist;
});
-
- // todo: assert that closing then re-opening the modal after an LLM err removes the err message
});
describe('on the generate data step', () => {
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 1f85dd8e998..5f6982de0cb 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
@@ -3,6 +3,7 @@ import { connect } from 'react-redux';
import {
css,
+ Body,
Button,
ButtonVariant,
ModalBody,
@@ -21,8 +22,8 @@ import {
generateFakerMappings,
mockDataGeneratorPreviousButtonClicked,
} from '../../modules/collection-tab';
-import { default as SchemaConfirmationScreen } from './raw-schema-confirmation';
-import FakerSchemaEditor from './faker-schema-editor';
+import RawSchemaConfirmationScreen from './raw-schema-confirmation-screen';
+import FakerSchemaEditorScreen from './faker-schema-editor-screen';
import ScriptScreen from './script-screen';
const footerStyles = css`
@@ -36,6 +37,11 @@ const rightButtonsStyles = css`
flex-direction: row;
`;
+const namespaceStyles = css({
+ marginTop: spacing[200],
+ marginBottom: spacing[400],
+});
+
interface Props {
isOpen: boolean;
onClose: () => void;
@@ -43,6 +49,7 @@ interface Props {
onNextStep: () => void;
onConfirmSchema: () => Promise
;
onPreviousStep: () => void;
+ namespace: string;
}
const MockDataGeneratorModal = ({
@@ -52,13 +59,14 @@ const MockDataGeneratorModal = ({
onNextStep,
onConfirmSchema,
onPreviousStep,
+ namespace,
}: Props) => {
const modalBodyContent = useMemo(() => {
switch (currentStep) {
case MockDataGeneratorStep.SCHEMA_CONFIRMATION:
- return ;
+ return ;
case MockDataGeneratorStep.SCHEMA_EDITOR:
- return ;
+ return ;
case MockDataGeneratorStep.DOCUMENT_COUNT:
return <>>; // TODO: CLOUDP-333856
case MockDataGeneratorStep.PREVIEW_DATA:
@@ -78,6 +86,9 @@ const MockDataGeneratorModal = ({
}
};
+ const shouldShowNamespace =
+ currentStep !== MockDataGeneratorStep.GENERATE_DATA;
+
return (
+ {shouldShowNamespace && (
+ {namespace}
+ )}
{modalBodyContent}
@@ -120,6 +134,7 @@ const MockDataGeneratorModal = ({
const mapStateToProps = (state: CollectionState) => ({
isOpen: state.mockDataGenerator.isModalOpen,
currentStep: state.mockDataGenerator.currentStep,
+ namespace: state.namespace,
});
const ConnectedMockDataGeneratorModal = connect(mapStateToProps, {
diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/raw-schema-confirmation-screen.tsx b/packages/compass-collection/src/components/mock-data-generator-modal/raw-schema-confirmation-screen.tsx
new file mode 100644
index 00000000000..e55182e192c
--- /dev/null
+++ b/packages/compass-collection/src/components/mock-data-generator-modal/raw-schema-confirmation-screen.tsx
@@ -0,0 +1,119 @@
+import React from 'react';
+import { connect } from 'react-redux';
+
+import {
+ css,
+ palette,
+ spacing,
+ Banner,
+ BannerVariant,
+ Body,
+ DocumentList,
+} from '@mongodb-js/compass-components';
+
+import { usePreference } from 'compass-preferences-model/provider';
+import toSimplifiedFieldInfo from './to-simplified-field-info';
+import type { CollectionState } from '../../modules/collection-tab';
+import type { SchemaAnalysisState } from '../../schema-analysis-types';
+import type { MockDataGeneratorState } from './types';
+import HadronDocument from 'hadron-document';
+
+interface RawSchemaConfirmationScreenProps {
+ schemaAnalysis: SchemaAnalysisState;
+ fakerSchemaGenerationStatus: MockDataGeneratorState['status'];
+}
+
+const documentContainerStyles = css({
+ backgroundColor: palette.gray.light3,
+ border: `1px solid ${palette.gray.light2}`,
+ borderRadius: spacing[400],
+});
+
+const documentStyles = css({
+ padding: `${spacing[400]}px ${spacing[900]}px`,
+});
+
+const descriptionStyles = css({
+ marginBottom: spacing[200],
+});
+
+const errorBannerStyles = css({
+ marginTop: spacing[400],
+});
+
+const errorBannerTextStyles = css({
+ color: palette.red.dark2,
+});
+
+const RawSchemaConfirmationScreen = ({
+ schemaAnalysis,
+ fakerSchemaGenerationStatus,
+}: RawSchemaConfirmationScreenProps) => {
+ const enableSampleDocumentPassing = usePreference(
+ 'enableGenAISampleDocumentPassing'
+ );
+
+ const subtitleText = enableSampleDocumentPassing
+ ? 'Sample Documents Collected'
+ : 'Document Schema Identified';
+
+ const descriptionText = enableSampleDocumentPassing
+ ? 'A sample of documents from your collection will be sent to an LLM for processing.'
+ : 'We have identified the following schema from your documents. This schema will be sent to an LLM for processing.';
+
+ return (
+
+ {schemaAnalysis.status === 'complete' ? (
+ <>
+
+ {subtitleText}
+
+ {descriptionText}
+
+
+
+ {fakerSchemaGenerationStatus === 'error' && (
+
+
+ LLM Request failed. Please confirm again.
+
+
+ )}
+ >
+ ) : (
+ // Not reachable since schema analysis must be finished before the modal can be opened
+ We are analyzing your collection.
+ )}
+
+ );
+};
+
+const mapStateToProps = (state: CollectionState) => {
+ const schemaAnalysis = state.schemaAnalysis;
+ const fakerSchemaGenerationStatus = state.fakerSchemaGeneration.status;
+
+ return {
+ schemaAnalysis,
+ fakerSchemaGenerationStatus,
+ };
+};
+
+const ConnectedRawSchemaConfirmationScreen = connect(
+ mapStateToProps,
+ {}
+)(RawSchemaConfirmationScreen);
+
+export default ConnectedRawSchemaConfirmationScreen;
diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/raw-schema-confirmation.tsx b/packages/compass-collection/src/components/mock-data-generator-modal/raw-schema-confirmation.tsx
deleted file mode 100644
index 6848d5b8c97..00000000000
--- a/packages/compass-collection/src/components/mock-data-generator-modal/raw-schema-confirmation.tsx
+++ /dev/null
@@ -1,52 +0,0 @@
-import React from 'react';
-import { connect } from 'react-redux';
-
-import { Code, Body, Subtitle } from '@mongodb-js/compass-components';
-
-import type { CollectionState } from '../../modules/collection-tab';
-import type { FieldInfo } from '../../schema-analysis-types';
-
-interface RawSchemaConfirmationProps {
- schemaContent: Record | null;
- namespace: string;
-}
-
-// Note: Currently a placeholder. The final contents will be addressed by CLOUDP-333852
-const RawSchemaConfirmation = (props: RawSchemaConfirmationProps) => {
- // this will change
- const codeContent = props.schemaContent
- ? JSON.stringify(props.schemaContent, null, 4)
- : 'No schema data available';
-
- return (
-
- {props.namespace}
- Document Schema Identified
-
- We have identified the following schema from your documents. This schema
- will be sent to an LLM for processing.
-
-
- {codeContent}
-
-
- );
-};
-
-const mapStateToProps = (state: CollectionState) => {
- const schemaContent =
- state.schemaAnalysis.status === 'complete'
- ? state.schemaAnalysis.processedSchema
- : null;
- return {
- schemaContent,
- namespace: state.namespace,
- };
-};
-
-const ConnectedRawSchemaConfirmation = connect(
- mapStateToProps,
- {}
-)(RawSchemaConfirmation);
-
-export default ConnectedRawSchemaConfirmation;
diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/to-simplified-field-info.spec.ts b/packages/compass-collection/src/components/mock-data-generator-modal/to-simplified-field-info.spec.ts
new file mode 100644
index 00000000000..36040a301ef
--- /dev/null
+++ b/packages/compass-collection/src/components/mock-data-generator-modal/to-simplified-field-info.spec.ts
@@ -0,0 +1,256 @@
+import { expect } from 'chai';
+import toSimplifiedFieldInfo from './to-simplified-field-info';
+import type { SimplifiedFieldInfoTree } from './to-simplified-field-info';
+
+describe('toSimplifiedFieldInfo', function () {
+ it('simple case with minimal nesting and no arrays', function () {
+ const input = {
+ 'user.name': {
+ type: 'String' as const,
+ sample_values: ['John'],
+ probability: 1.0,
+ },
+ 'user.age': {
+ type: 'Number' as const,
+ sample_values: [25, 30],
+ probability: 0.8,
+ },
+ 'user.profile.bio': {
+ type: 'String' as const,
+ sample_values: ['Software engineer'],
+ probability: 0.9,
+ },
+ 'user.profile.isVerified': {
+ type: 'Boolean' as const,
+ sample_values: [true, false],
+ probability: 0.7,
+ },
+ 'metadata.createdAt': {
+ type: 'Date' as const,
+ sample_values: [new Date('2023-01-01')],
+ probability: 1.0,
+ },
+ 'metadata.objectId': {
+ type: 'ObjectId' as const,
+ sample_values: ['642d766b7300158b1f22e972'],
+ probability: 1.0,
+ },
+ };
+
+ const result = toSimplifiedFieldInfo(input);
+
+ const expected: SimplifiedFieldInfoTree = {
+ user: {
+ name: 'String',
+ age: 'Number',
+ profile: {
+ bio: 'String',
+ isVerified: 'Boolean',
+ },
+ },
+ metadata: {
+ createdAt: 'Date',
+ objectId: 'ObjectId',
+ },
+ };
+
+ expect(result).to.deep.equal(expected);
+ });
+
+ it('handles nested arrays of primitives', function () {
+ const input = {
+ 'tags[]': {
+ type: 'String' as const,
+ sample_values: ['red', 'blue', 'green'],
+ probability: 1.0,
+ },
+ 'scores[]': {
+ type: 'Number' as const,
+ sample_values: [85, 92, 78],
+ probability: 0.9,
+ },
+ 'matrix[][]': {
+ type: 'Number' as const,
+ sample_values: [1, 2, 3, 4],
+ probability: 1.0,
+ },
+ 'flags[]': {
+ type: 'Boolean' as const,
+ sample_values: [true, false],
+ probability: 0.8,
+ },
+ 'timestamps[]': {
+ type: 'Date' as const,
+ sample_values: [new Date('2023-01-01'), new Date('2023-06-15')],
+ probability: 0.7,
+ },
+ 'ids[]': {
+ type: 'ObjectId' as const,
+ sample_values: ['642d766b7300158b1f22e972', '642d766b7300158b1f22e973'],
+ probability: 1.0,
+ },
+ };
+
+ const result = toSimplifiedFieldInfo(input);
+
+ const expected: SimplifiedFieldInfoTree = {
+ 'tags[]': 'String',
+ 'scores[]': 'Number',
+ 'matrix[][]': 'Number',
+ 'flags[]': 'Boolean',
+ 'timestamps[]': 'Date',
+ 'ids[]': 'ObjectId',
+ };
+
+ expect(result).to.deep.equal(expected);
+ });
+
+ it('handles nested arrays of documents', function () {
+ const input = {
+ 'items[].id': {
+ type: 'Number' as const,
+ sample_values: [1, 2],
+ probability: 1.0,
+ },
+ 'items[].name': {
+ type: 'String' as const,
+ sample_values: ['Item A', 'Item B'],
+ probability: 1.0,
+ },
+ 'items[].metadata.createdBy': {
+ type: 'String' as const,
+ sample_values: ['admin', 'user'],
+ probability: 0.9,
+ },
+ 'items[].metadata.tags[]': {
+ type: 'String' as const,
+ sample_values: ['urgent', 'review', 'approved'],
+ probability: 0.8,
+ },
+ 'items[].price': {
+ type: 'Decimal128' as const,
+ sample_values: [19.99, 29.99],
+ probability: 0.95,
+ },
+ 'items[].binary': {
+ type: 'Binary' as const,
+ sample_values: ['dGVzdA=='],
+ probability: 0.3,
+ },
+ };
+
+ const result = toSimplifiedFieldInfo(input);
+
+ const expected: SimplifiedFieldInfoTree = {
+ 'items[]': {
+ id: 'Number',
+ name: 'String',
+ metadata: {
+ createdBy: 'String',
+ 'tags[]': 'String',
+ },
+ price: 'Decimal128',
+ binary: 'Binary',
+ },
+ };
+
+ expect(result).to.deep.equal(expected);
+ });
+
+ it('handles nested arrays of arrays', function () {
+ // Input based on complex nested array structures
+ const input = {
+ 'cube[][][]': {
+ type: 'Number' as const,
+ sample_values: [1, 2, 3, 4, 5, 6, 7, 8],
+ probability: 1.0,
+ },
+ 'matrix[][].x': {
+ type: 'Number' as const,
+ sample_values: [1, 3],
+ probability: 1.0,
+ },
+ 'matrix[][].y': {
+ type: 'Number' as const,
+ sample_values: [2, 4],
+ probability: 1.0,
+ },
+ 'teams[].members[]': {
+ type: 'String' as const,
+ sample_values: ['Alice', 'Bob', 'Charlie'],
+ probability: 1.0,
+ },
+ 'teams[].name': {
+ type: 'String' as const,
+ sample_values: ['Team A', 'Team B'],
+ probability: 1.0,
+ },
+ 'complex[][].data[]': {
+ type: 'Long' as const,
+ sample_values: [123456789, 987654321],
+ probability: 0.9,
+ },
+ 'complex[][].regex': {
+ type: 'RegExp' as const,
+ sample_values: ['pattern'],
+ probability: 0.6,
+ },
+ 'complex[][].code': {
+ type: 'Code' as const,
+ sample_values: ['function() {}'],
+ probability: 0.4,
+ },
+ 'nested[][].symbols[]': {
+ type: 'Symbol' as const,
+ sample_values: ['symbol1', 'symbol2'],
+ probability: 0.5,
+ },
+ 'timestamps[][].created': {
+ type: 'Timestamp' as const,
+ sample_values: [4294967297],
+ probability: 0.8,
+ },
+ 'keys[][].max': {
+ type: 'MaxKey' as const,
+ sample_values: ['MaxKey'],
+ probability: 0.2,
+ },
+ 'keys[][].min': {
+ type: 'MinKey' as const,
+ sample_values: ['MinKey'],
+ probability: 0.2,
+ },
+ };
+
+ const result = toSimplifiedFieldInfo(input);
+
+ const expected: SimplifiedFieldInfoTree = {
+ 'cube[][][]': 'Number',
+ 'matrix[][]': {
+ x: 'Number',
+ y: 'Number',
+ },
+ 'teams[]': {
+ 'members[]': 'String',
+ name: 'String',
+ },
+ 'complex[][]': {
+ 'data[]': 'Long',
+ regex: 'RegExp',
+ code: 'Code',
+ },
+ 'nested[][]': {
+ 'symbols[]': 'Symbol',
+ },
+ 'timestamps[][]': {
+ created: 'Timestamp',
+ },
+ 'keys[][]': {
+ max: 'MaxKey',
+ min: 'MinKey',
+ },
+ };
+
+ expect(result).to.deep.equal(expected);
+ });
+});
diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/to-simplified-field-info.ts b/packages/compass-collection/src/components/mock-data-generator-modal/to-simplified-field-info.ts
new file mode 100644
index 00000000000..056dad9d670
--- /dev/null
+++ b/packages/compass-collection/src/components/mock-data-generator-modal/to-simplified-field-info.ts
@@ -0,0 +1,48 @@
+import { FIELD_NAME_SEPARATOR } from '../../transform-schema-to-field-info';
+import type { processSchema } from '../../transform-schema-to-field-info';
+import type { FieldInfo } from '../../schema-analysis-types';
+
+type UserFriendlyFieldInfoNode =
+ | { [field: string]: UserFriendlyFieldInfoNode }
+ | FieldInfo['type'];
+export type SimplifiedFieldInfoTree = {
+ [field: string]: UserFriendlyFieldInfoNode;
+};
+
+/**
+ * Usage is for display purposes only. The result is derived from the work of `processSchema`,
+ * ensuring that the user sees a simplification of what the LLM processes.
+ */
+export default function toSimplifiedFieldInfo(
+ input: ReturnType
+): SimplifiedFieldInfoTree {
+ // ensure parent nodes are created before their children
+ const sortedFieldPaths = Object.keys(input).sort(
+ (f1, f2) => countSeparators(f1) - countSeparators(f2)
+ );
+
+ const result: SimplifiedFieldInfoTree = {};
+ for (const path of sortedFieldPaths) {
+ const fieldParts = path.split(FIELD_NAME_SEPARATOR);
+
+ let node = result;
+ for (let i = 0; i < fieldParts.length; i++) {
+ const part = fieldParts[i];
+
+ if (i === fieldParts.length - 1) {
+ node[part] = input[path].type;
+ break;
+ }
+
+ if (typeof node[part] !== 'object' || node[part] === null) {
+ node[part] = {};
+ }
+ node = node[part];
+ }
+ }
+ return result;
+}
+
+function countSeparators(input: string): number {
+ return input.split(FIELD_NAME_SEPARATOR).length - 1;
+}
diff --git a/packages/compass-collection/src/modules/collection-tab.ts b/packages/compass-collection/src/modules/collection-tab.ts
index e1f3afa2eac..8c7323045f6 100644
--- a/packages/compass-collection/src/modules/collection-tab.ts
+++ b/packages/compass-collection/src/modules/collection-tab.ts
@@ -32,7 +32,10 @@ import {
type FieldInfo,
} from '../schema-analysis-types';
import { calculateSchemaDepth } from '../calculate-schema-depth';
-import { processSchema } from '../transform-schema-to-field-info';
+import {
+ processSchema,
+ ProcessSchemaUnsupportedStateError,
+} 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';
@@ -51,6 +54,13 @@ function isAction(
const ERROR_CODE_MAX_TIME_MS_EXPIRED = 50;
function getErrorDetails(error: Error): SchemaAnalysisError {
+ if (error instanceof ProcessSchemaUnsupportedStateError) {
+ return {
+ errorType: 'unsupportedState',
+ errorMessage: error.message,
+ };
+ }
+
const errorCode = (error as MongoError).code;
const errorMessage = error.message || 'Unknown error';
let errorType: SchemaAnalysisError['errorType'] = 'general';
diff --git a/packages/compass-collection/src/schema-analysis-types.ts b/packages/compass-collection/src/schema-analysis-types.ts
index 83a501ceb20..286fc166158 100644
--- a/packages/compass-collection/src/schema-analysis-types.ts
+++ b/packages/compass-collection/src/schema-analysis-types.ts
@@ -22,7 +22,7 @@ export type SchemaAnalysisStartedState = {
export type SchemaAnalysisError = {
errorMessage: string;
- errorType: 'timeout' | 'highComplexity' | 'general';
+ errorType: 'timeout' | 'highComplexity' | 'general' | 'unsupportedState';
};
export type SchemaAnalysisErrorState = {
diff --git a/packages/compass-collection/src/transform-schema-to-field-info.spec.ts b/packages/compass-collection/src/transform-schema-to-field-info.spec.ts
index 04002bcda05..06bd64de345 100644
--- a/packages/compass-collection/src/transform-schema-to-field-info.spec.ts
+++ b/packages/compass-collection/src/transform-schema-to-field-info.spec.ts
@@ -1105,4 +1105,89 @@ describe('processSchema', function () {
},
});
});
+
+ /**
+ * Verifies malformed field paths can be caught by bugs in the construction logic.
+ * These are unlikely to occur with valid `Schema` inputs to `processSchema`.
+ */
+ describe('validateFieldPath error conditions', function () {
+ it('throws error for empty field parts', function () {
+ const schema: Schema = {
+ fields: [
+ {
+ name: 'parent',
+ path: ['parent'],
+ count: 1,
+ type: ['Document'],
+ probability: 1.0,
+ hasDuplicates: false,
+ types: [
+ {
+ name: 'Document',
+ bsonType: 'Document',
+ path: ['parent'],
+ count: 1,
+ probability: 1.0,
+ fields: [
+ {
+ name: '', // Empty field name
+ path: ['parent', ''],
+ count: 1,
+ type: ['String'],
+ probability: 1.0,
+ hasDuplicates: false,
+ types: [
+ {
+ name: 'String',
+ bsonType: 'String',
+ path: ['parent', ''],
+ count: 1,
+ probability: 1.0,
+ values: ['test'],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ count: 1,
+ };
+
+ expect(() => processSchema(schema)).to.throw(
+ "invalid fieldPath 'parent.': field parts cannot be empty"
+ );
+ });
+
+ it('throws error for a field part that only contains "[]"', function () {
+ const schema: Schema = {
+ fields: [
+ {
+ name: '[]', // Field name is just "[]"
+ path: ['[]'],
+ count: 1,
+ type: ['String'],
+ probability: 1.0,
+ hasDuplicates: false,
+ types: [
+ {
+ name: 'String',
+ bsonType: 'String',
+ path: ['[]'],
+ count: 1,
+ probability: 1.0,
+ values: ['test'],
+ },
+ ],
+ },
+ ],
+ count: 1,
+ };
+
+ expect(() => processSchema(schema)).to.throw(
+ "invalid fieldPath '[]': field parts must have characters other than '[]'"
+ );
+ });
+ });
});
diff --git a/packages/compass-collection/src/transform-schema-to-field-info.ts b/packages/compass-collection/src/transform-schema-to-field-info.ts
index 9d782c7a187..f88dfb1e6a1 100644
--- a/packages/compass-collection/src/transform-schema-to-field-info.ts
+++ b/packages/compass-collection/src/transform-schema-to-field-info.ts
@@ -43,6 +43,21 @@ import {
* Maximum number of sample values to include for each field
*/
const MAX_SAMPLE_VALUES = 10;
+export const FIELD_NAME_SEPARATOR = '.';
+
+export class ProcessSchemaUnsupportedStateError extends Error {
+ constructor(message: string) {
+ super(message);
+ this.name = 'ProcessSchemaUnsupportedStateError';
+ }
+}
+
+export class ProcessSchemaValidationError extends Error {
+ constructor(message: string) {
+ super(message);
+ this.name = 'ProcessSchemaValidationError';
+ }
+}
/**
* Converts a BSON value to its primitive JavaScript equivalent
@@ -122,6 +137,8 @@ function isPrimitiveSchemaType(type: SchemaType): type is PrimitiveSchemaType {
/**
* Transforms a raw mongodb-schema Schema into a flat Record
* using dot notation for nested fields and bracket notation for arrays.
+ *
+ * The result is used for the Mock Data Generator LLM call.
*/
export function processSchema(schema: Schema): Record {
const result: Record = {};
@@ -135,6 +152,11 @@ export function processSchema(schema: Schema): Record {
processNamedField(field, '', result);
}
+ // post-processing validation
+ for (const fieldPath of Object.keys(result)) {
+ validateFieldPath(fieldPath);
+ }
+
return result;
}
@@ -156,6 +178,12 @@ function processNamedField(
return;
}
+ if (field.name.includes(FIELD_NAME_SEPARATOR)) {
+ throw new ProcessSchemaUnsupportedStateError(
+ `no support for field names that contain a '${FIELD_NAME_SEPARATOR}' ; field name: '${field.name}'`
+ );
+ }
+
const currentPath = pathPrefix ? `${pathPrefix}.${field.name}` : field.name;
// Process based on the type
@@ -221,3 +249,25 @@ function getMostFrequentType(types: SchemaType[]): SchemaType | null {
return validTypes[0] || null;
}
+
+/**
+ * Note: This validation takes a defensive stance. As illustrated by the unit tests, malformed
+ * inputs are required to simulate these unlikely errors.
+ */
+function validateFieldPath(fieldPath: string) {
+ const parts = fieldPath.split(FIELD_NAME_SEPARATOR);
+
+ for (const part of parts) {
+ if (part === '') {
+ throw new ProcessSchemaValidationError(
+ `invalid fieldPath '${fieldPath}': field parts cannot be empty`
+ );
+ }
+
+ if (part.replaceAll('[]', '') === '') {
+ throw new ProcessSchemaValidationError(
+ `invalid fieldPath '${fieldPath}': field parts must have characters other than '[]'`
+ );
+ }
+ }
+}
diff --git a/packages/compass-components/src/components/document-list/document.tsx b/packages/compass-components/src/components/document-list/document.tsx
index 71368e43955..64788bf7c9e 100644
--- a/packages/compass-components/src/components/document-list/document.tsx
+++ b/packages/compass-components/src/components/document-list/document.tsx
@@ -88,6 +88,7 @@ const HadronDocument: React.FunctionComponent<{
extraGutterWidth?: number;
onUpdateQuery?: (field: string, value: unknown) => void;
query?: Record;
+ className?: string;
}> = ({
value: document,
editable = false,
@@ -96,6 +97,7 @@ const HadronDocument: React.FunctionComponent<{
extraGutterWidth,
onUpdateQuery,
query,
+ className = '',
}) => {
const { elements, visibleElements } = useHadronDocument(document);
const [autoFocus, setAutoFocus] = useState<{
@@ -130,7 +132,7 @@ const HadronDocument: React.FunctionComponent<{
);
return (
-