Skip to content
Merged
40 changes: 35 additions & 5 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,10 @@ 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';

const loadingContainerStyles = css({
width: '100%',
Expand Down Expand Up @@ -73,17 +75,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 +106,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: Edit) => void;
}> = ({
step,
hasUndo,
onUndoClick,
hasRedo,
onRedoClick,
model,
editErrors,
onRetryClick,
onCancelClick,
onApplyClick,
Expand All @@ -118,6 +133,43 @@ const DiagramEditor: React.FunctionComponent<{
}
}, [applyInput]);

const applyPlaceholder =
(type: 'AddRelationship' | 'RemoveRelationship') => () => {
let placeholder = {};
switch (type) {
case 'AddRelationship':
placeholder = {
type: 'AddRelationship',
relationship: {
id: 'relationship1',
relationship: [
{
ns: 'db.sourceCollection',
cardinality: 1,
fields: ['field1'],
},
{
ns: 'db.targetCollection',
cardinality: 1,
fields: ['field2'],
},
],
isInferred: false,
},
};
break;
case 'RemoveRelationship':
placeholder = {
type: 'RemoveRelationship',
relationshipId: 'relationship1',
};
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 +224,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 +246,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 +305,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
Expand Up @@ -24,7 +24,7 @@ class DataModelStorageElectron implements DataModelStorage {
async loadAll(): Promise<MongoDBDataModelDescription[]> {
try {
const res = await this.userData.readAll();
return res.data;
return res.data as MongoDBDataModelDescription[];
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

How I ran into this: I felt like it's unnecessarily complex to go deep with the zod schemas and basically re-define MongoDBJsonSchema.
But having it typed in TS does make sense. So it has to be cast somehow. I suppose I could go into more detail here, but I figured this would be enough for the load. Let me know if you have a better idea

} catch (err) {
return [];
}
Expand Down
104 changes: 99 additions & 5 deletions packages/compass-data-modeling/src/services/data-model-storage.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,96 @@
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(),
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.unknown(), // skipped for simplicity
Copy link
Collaborator

@gribnoysup gribnoysup May 21, 2025

Choose a reason for hiding this comment

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

I understand that we probably don't want a full blown validation of MongoDBJSONSchema type implemented here, but I think we might want to consider using z.custom here, provide the MongoDBJSONSchema as a generic argument and maybe adding some very basic validation for the type matching. The purpose of these z.<type> helpers is not only to get the type definition after all, but to actually build a validator that makes sure that data, in our case, you're loading from disk (and at some point user will be passing to the app) is matching the shape that we need for this to function.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

oh thank you! this makes it so much smoother 🫶

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 = Omit<
z.output<typeof StaticModelSchema>,
'collections'
> & {
collections: Array<
Omit<
z.output<typeof StaticModelSchema>['collections'][number],
'jsonSchema'
> & {
jsonSchema: MongoDBJSONSchema;
}
>;
};

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

type BaseEdit = z.output<typeof EditSchema>;
type SetModelEdit = Omit<Extract<BaseEdit, { type: 'SetModel' }>, 'model'> & {
model: StaticModel;
};

export type Edit =
| SetModelEdit
| Extract<BaseEdit, { type: 'AddRelationship' | 'RemoveRelationship' }>;

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,13 +103,15 @@ 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<
typeof MongoDBDataModelDescriptionSchema
>;
export type MongoDBDataModelDescription = Omit<
z.output<typeof MongoDBDataModelDescriptionSchema>,
'edits'
> & {
edits: Array<Edit>;
};

export interface DataModelStorage {
save(description: MongoDBDataModelDescription): Promise<boolean>;
Expand Down
Loading
Loading