Skip to content
Merged
28 changes: 21 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,11 @@ import {
spacing,
Button,
palette,
ErrorSummary,
} from '@mongodb-js/compass-components';
import { cancelAnalysis, retryAnalysis } from '../store/analysis-process';
import type { Edit, StaticModel } from '../services/data-model-storage';
import { UUID } from 'bson';

const loadingContainerStyles = css({
width: '100%',
Expand Down Expand Up @@ -73,17 +76,29 @@ const modelPreviewStyles = css({
});

const editorContainerStyles = css({
height: 160 + 34 + 16,
display: 'flex',
flexDirection: 'column',
gap: 8,
boxShadow: `0 0 0 2px ${palette.gray.light2}`,
});

const editorContainerApplyButtonStyles = css({
const editorContainerApplyContainerStyles = css({
paddingLeft: 8,
paddingRight: 8,
alignSelf: 'flex-end',
justifyContent: 'flex-end',
gap: spacing[200],
display: 'flex',
width: '100%',
height: spacing[100],
});

const editorContainerPlaceholderButtonStyles = css({
paddingLeft: 8,
paddingRight: 8,
alignSelf: 'flex-start',
display: 'flex',
gap: spacing[200],
paddingTop: spacing[200],
});

const DiagramEditor: React.FunctionComponent<{
Expand All @@ -92,18 +107,19 @@ const DiagramEditor: React.FunctionComponent<{
onUndoClick: () => void;
hasRedo: boolean;
onRedoClick: () => void;
model: unknown;
model: StaticModel | null;
editErrors?: string[];
onRetryClick: () => void;
onCancelClick: () => void;
// TODO
onApplyClick: (edit: unknown) => void;
onApplyClick: (edit: Omit<Edit, 'id' | 'timestamp'>) => void;
}> = ({
step,
hasUndo,
onUndoClick,
hasRedo,
onRedoClick,
model,
editErrors,
onRetryClick,
onCancelClick,
onApplyClick,
Expand All @@ -118,6 +134,43 @@ const DiagramEditor: React.FunctionComponent<{
}
}, [applyInput]);

const applyPlaceholder =
(type: 'AddRelationship' | 'RemoveRelationship') => () => {
let placeholder = {};
switch (type) {
case 'AddRelationship':
placeholder = {
type: 'AddRelationship',
relationship: {
id: new UUID().toString(),
relationship: [
{
ns: 'db.sourceCollection',
cardinality: 1,
fields: ['field1'],
},
{
ns: 'db.targetCollection',
cardinality: 1,
fields: ['field2'],
},
],
isInferred: false,
},
};
break;
case 'RemoveRelationship':
placeholder = {
type: 'RemoveRelationship',
relationshipId: new UUID().toString(),
};
break;
default:
throw new Error(`Unknown placeholder ${placeholder}`);
}
setApplyInput(JSON.stringify(placeholder, null, 2));
};

const modelStr = useMemo(() => {
return JSON.stringify(model, null, 2);
}, [model]);
Expand Down Expand Up @@ -172,6 +225,20 @@ const DiagramEditor: React.FunctionComponent<{
></CodemirrorMultilineEditor>
</div>
<div className={editorContainerStyles} data-testid="apply-editor">
<div className={editorContainerPlaceholderButtonStyles}>
<Button
onClick={applyPlaceholder('AddRelationship')}
data-testid="placeholder-addrelationship-button"
>
Add relationship
</Button>
<Button
onClick={applyPlaceholder('RemoveRelationship')}
data-testid="placeholder-removerelationship-button"
>
Remove relationship
</Button>
</div>
<div>
<CodemirrorMultilineEditor
language="json"
Expand All @@ -180,7 +247,8 @@ const DiagramEditor: React.FunctionComponent<{
maxLines={10}
></CodemirrorMultilineEditor>
</div>
<div className={editorContainerApplyButtonStyles}>
<div className={editorContainerApplyContainerStyles}>
{editErrors && <ErrorSummary errors={editErrors} />}
<Button
onClick={() => {
onApplyClick(JSON.parse(applyInput));
Expand Down Expand Up @@ -238,6 +306,7 @@ export default connect(
model: diagram
? selectCurrentModel(getCurrentDiagramFromState(state))
: null,
editErrors: diagram?.editErrors,
};
},
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,81 @@
import { z } from '@mongodb-js/compass-user-data';
import type { MongoDBJSONSchema } from 'mongodb-schema';

export const RelationshipSideSchema = z.object({
ns: z.string(),
cardinality: z.number(),
fields: z.array(z.string()),
});

export type RelationshipSide = z.output<typeof RelationshipSideSchema>;

export const RelationshipSchema = z.object({
id: z.string().uuid(),
relationship: z.tuple([RelationshipSideSchema, RelationshipSideSchema]),
isInferred: z.boolean(),
});

export type Relationship = z.output<typeof RelationshipSchema>;

export const StaticModelSchema = z.object({
collections: z.array(
z.object({
ns: z.string(),
jsonSchema: z.custom<MongoDBJSONSchema>((value) => {
const isObject = typeof value === 'object' && value !== null;
return isObject && 'bsonType' in value;
}),
indexes: z.array(z.record(z.unknown())),
shardKey: z.record(z.unknown()).optional(),
displayPosition: z.tuple([z.number(), z.number()]),
})
),
relationships: z.array(RelationshipSchema),
});

export type StaticModel = z.output<typeof StaticModelSchema>;

const EditSchemaBase = z.object({
id: z.string().uuid(),
timestamp: z.string().datetime(),
});

const EditSchemaVariants = z.discriminatedUnion('type', [
z.object({
type: z.literal('SetModel'),
model: StaticModelSchema,
}),
z.object({
type: z.literal('AddRelationship'),
relationship: RelationshipSchema,
}),
z.object({
type: z.literal('RemoveRelationship'),
relationshipId: z.string().uuid(),
}),
]);

export const EditSchema = z.intersection(EditSchemaBase, EditSchemaVariants);

export type Edit = z.output<typeof EditSchema>;

export const validateEdit = (
edit: unknown
): { result: true; errors?: never } | { result: false; errors: string[] } => {
try {
EditSchema.parse(edit);
return { result: true };
} catch (e) {
return {
result: false,
errors: (e as z.ZodError).issues.map(({ path, message }) =>
message === 'Required'
? `'${path}' is required`
: `Invalid field '${path}': ${message}`
),
};
}
};

export const MongoDBDataModelDescriptionSchema = z.object({
id: z.string(),
Expand All @@ -11,8 +88,7 @@ export const MongoDBDataModelDescriptionSchema = z.object({
*/
connectionId: z.string().nullable(),

// TODO: define rest of the schema based on arch doc / tech design
edits: z.array(z.unknown()).default([]),
edits: z.array(EditSchema).default([]),
});

export type MongoDBDataModelDescription = z.output<
Expand Down
14 changes: 7 additions & 7 deletions packages/compass-data-modeling/src/store/analysis-process.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import type { Reducer } from 'redux';
import { isAction } from './util';
import type { DataModelingThunkAction } from './reducer';
import { analyzeDocuments } from 'mongodb-schema';
import { analyzeDocuments, type MongoDBJSONSchema } from 'mongodb-schema';
import { getCurrentDiagramFromState } from './diagram';
import type { Document } from 'bson';
import type { AggregationCursor } from 'mongodb';
import type { Relationship } from '../services/data-model-storage';

export type AnalysisProcessState = {
currentAnalysisOptions:
Expand Down Expand Up @@ -61,9 +62,8 @@ export type AnalysisFinishedAction = {
type: AnalysisProcessActionTypes.ANALYSIS_FINISHED;
name: string;
connectionId: string;
// TODO
schema: Record<string, unknown>;
relations: unknown[];
collections: { ns: string; schema: MongoDBJSONSchema }[];
relations: Relationship[];
};

export type AnalysisFailedAction = {
Expand Down Expand Up @@ -157,7 +157,7 @@ export function startAnalysis(
try {
const dataService =
services.connections.getDataServiceForConnection(connectionId);
const schema = await Promise.all(
const collections = await Promise.all(
namespaces.map(async (ns) => {
const sample: AggregationCursor<Document> = dataService.sampleCursor(
ns,
Expand Down Expand Up @@ -188,7 +188,7 @@ export function startAnalysis(
type: AnalysisProcessActionTypes.NAMESPACE_SCHEMA_ANALYZED,
namespace: ns,
});
return [ns, schema];
return { ns, schema };
})
);
if (options.automaticallyInferRelations) {
Expand All @@ -201,7 +201,7 @@ export function startAnalysis(
type: AnalysisProcessActionTypes.ANALYSIS_FINISHED,
name,
connectionId,
schema: Object.fromEntries(schema),
collections,
relations: [],
});
void services.dataModelStorage.save(
Expand Down
Loading
Loading