From 0f448e3c0f518a9ba088eba18962a3610db72ea5 Mon Sep 17 00:00:00 2001 From: Paula Stachova Date: Fri, 9 May 2025 15:50:57 +0200 Subject: [PATCH 01/17] wip --- package-lock.json | 40 +++++++++++-- .../src/services/data-model-storage.ts | 56 ++++++++++++++++++- .../src/store/diagram.ts | 12 ++-- .../compass-preferences-model/package.json | 2 +- packages/compass-user-data/package.json | 2 +- 5 files changed, 98 insertions(+), 14 deletions(-) diff --git a/package-lock.json b/package-lock.json index c3f71980565..310ffed52de 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43237,6 +43237,7 @@ "version": "3.23.8", "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" @@ -46109,7 +46110,7 @@ "lodash": "^4.17.21", "react": "^17.0.2", "yargs-parser": "^21.1.1", - "zod": "^3.22.3" + "zod": "^3.24.4" }, "devDependencies": { "@mongodb-js/eslint-config-compass": "^1.3.8", @@ -46159,6 +46160,15 @@ "node": ">=12" } }, + "packages/compass-preferences-model/node_modules/zod": { + "version": "3.24.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.4.tgz", + "integrity": "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "packages/compass-query-bar": { "name": "@mongodb-js/compass-query-bar", "version": "8.57.0", @@ -47005,7 +47015,7 @@ "@mongodb-js/compass-logging": "^1.6.9", "@mongodb-js/compass-utils": "^0.8.8", "write-file-atomic": "^5.0.1", - "zod": "^3.22.3" + "zod": "^3.24.4" }, "devDependencies": { "@mongodb-js/eslint-config-compass": "^1.3.8", @@ -47075,6 +47085,15 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "packages/compass-user-data/node_modules/zod": { + "version": "3.24.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.4.tgz", + "integrity": "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "packages/compass-utils": { "name": "@mongodb-js/compass-utils", "version": "0.8.8", @@ -58856,7 +58875,7 @@ "sinon": "^9.2.3", "typescript": "^5.0.4", "write-file-atomic": "^5.0.1", - "zod": "^3.22.3" + "zod": "^3.24.4" }, "dependencies": { "diff": { @@ -58892,6 +58911,11 @@ "imurmurhash": "^0.1.4", "signal-exit": "^4.0.1" } + }, + "zod": { + "version": "3.24.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.4.tgz", + "integrity": "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==" } } }, @@ -68443,7 +68467,7 @@ "react": "^17.0.2", "sinon": "^9.2.3", "yargs-parser": "^21.1.1", - "zod": "^3.22.3" + "zod": "^3.24.4" }, "dependencies": { "sinon": { @@ -68472,6 +68496,11 @@ "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==" + }, + "zod": { + "version": "3.24.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.4.tgz", + "integrity": "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==" } } }, @@ -89129,7 +89158,8 @@ "zod": { "version": "3.23.8", "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", - "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==" + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "dev": true } } } diff --git a/packages/compass-data-modeling/src/services/data-model-storage.ts b/packages/compass-data-modeling/src/services/data-model-storage.ts index 7ba36c519b3..0bd26173f13 100644 --- a/packages/compass-data-modeling/src/services/data-model-storage.ts +++ b/packages/compass-data-modeling/src/services/data-model-storage.ts @@ -1,5 +1,58 @@ import { z } from '@mongodb-js/compass-user-data'; +export const RelationshipSideSchema = z.object({ + ns: z.string(), + cardinality: z.number(), + fields: z.array(z.string()), +}); + +export type RelationshipSide = z.output; + +export const RelationshipSchema = z.object({ + id: z.string(), + relationship: z.tuple([RelationshipSideSchema, RelationshipSideSchema]), + isInferred: z.boolean(), +}); + +export type Relationship = z.output; + +export const StaticModelSchema = z.object({ + collections: z.array( + z.object({ + ns: z.string(), + jsonSchema: z.unknown(), // MongoDBJSONSchema is not directly representable in zod + 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; + +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(), + }), +]); + +export type Edit = z.output; + export const MongoDBDataModelDescriptionSchema = z.object({ id: z.string(), name: z.string(), @@ -11,8 +64,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< diff --git a/packages/compass-data-modeling/src/store/diagram.ts b/packages/compass-data-modeling/src/store/diagram.ts index 7162de3febb..18fc20366ab 100644 --- a/packages/compass-data-modeling/src/store/diagram.ts +++ b/packages/compass-data-modeling/src/store/diagram.ts @@ -1,7 +1,10 @@ import type { Reducer } from 'redux'; import { UUID } from 'bson'; import { isAction } from './util'; -import type { MongoDBDataModelDescription } from '../services/data-model-storage'; +import type { + Edit, + MongoDBDataModelDescription, +} from '../services/data-model-storage'; import { AnalysisProcessActionTypes } from './analysis-process'; import { memoize } from 'lodash'; import type { DataModelingState, DataModelingThunkAction } from './reducer'; @@ -10,9 +13,9 @@ import { showConfirmation, showPrompt } from '@mongodb-js/compass-components'; export type DiagramState = | (Omit & { edits: { - prev: MongoDBDataModelDescription['edits'][]; - current: MongoDBDataModelDescription['edits']; - next: MongoDBDataModelDescription['edits'][]; + prev: Edit[]; + current: Edit[]; + next: Edit[]; }; }) | null; // null when no diagram is currently open @@ -44,7 +47,6 @@ export type RenameDiagramAction = { export type ApplyEditAction = { type: DiagramActionTypes.APPLY_EDIT; - // TODO edit: unknown; }; diff --git a/packages/compass-preferences-model/package.json b/packages/compass-preferences-model/package.json index 21c76dd4319..97ed5a3bb8b 100644 --- a/packages/compass-preferences-model/package.json +++ b/packages/compass-preferences-model/package.json @@ -60,7 +60,7 @@ "lodash": "^4.17.21", "react": "^17.0.2", "yargs-parser": "^21.1.1", - "zod": "^3.22.3" + "zod": "^3.24.4" }, "devDependencies": { "@mongodb-js/eslint-config-compass": "^1.3.8", diff --git a/packages/compass-user-data/package.json b/packages/compass-user-data/package.json index f9c5d72042f..a924da9d13a 100644 --- a/packages/compass-user-data/package.json +++ b/packages/compass-user-data/package.json @@ -52,7 +52,7 @@ "@mongodb-js/compass-logging": "^1.6.9", "@mongodb-js/compass-utils": "^0.8.8", "write-file-atomic": "^5.0.1", - "zod": "^3.22.3" + "zod": "^3.24.4" }, "devDependencies": { "@mongodb-js/eslint-config-compass": "^1.3.8", From 1ff361c3457a5ff4b72c8469a6aa064a9282277b Mon Sep 17 00:00:00 2001 From: Paula Stachova Date: Mon, 19 May 2025 11:29:23 +0200 Subject: [PATCH 02/17] wip --- .../src/services/data-model-storage.ts | 2 +- .../src/store/diagram.ts | 99 ++++++++++--------- 2 files changed, 51 insertions(+), 50 deletions(-) diff --git a/packages/compass-data-modeling/src/services/data-model-storage.ts b/packages/compass-data-modeling/src/services/data-model-storage.ts index 0bd26173f13..918f55e4011 100644 --- a/packages/compass-data-modeling/src/services/data-model-storage.ts +++ b/packages/compass-data-modeling/src/services/data-model-storage.ts @@ -36,7 +36,7 @@ export const EditSchema = z.discriminatedUnion('type', [ type: z.literal('SetModel'), id: z.string(), timestamp: z.string(), - model: StaticModelSchema, + model: z.unknown(), // TODO: StaticModelSchema, }), z.object({ type: z.literal('AddRelationship'), diff --git a/packages/compass-data-modeling/src/store/diagram.ts b/packages/compass-data-modeling/src/store/diagram.ts index 18fc20366ab..ba941f98726 100644 --- a/packages/compass-data-modeling/src/store/diagram.ts +++ b/packages/compass-data-modeling/src/store/diagram.ts @@ -47,6 +47,7 @@ export type RenameDiagramAction = { export type ApplyEditAction = { type: DiagramActionTypes.APPLY_EDIT; + // TODO edit: unknown; }; @@ -85,27 +86,27 @@ export const diagramReducer: Reducer = ( }; } - if (isAction(action, AnalysisProcessActionTypes.ANALYSIS_FINISHED)) { - return { - id: new UUID().toString(), - name: action.name, - connectionId: action.connectionId, - edits: { - prev: [], - current: [ - { - // TODO - type: 'SetModel', - model: { - schema: action.schema, - relations: action.relations, - }, - }, - ], - next: [], - }, - }; - } + // if (isAction(action, AnalysisProcessActionTypes.ANALYSIS_FINISHED)) { + // return { + // id: new UUID().toString(), + // name: action.name, + // connectionId: action.connectionId, + // edits: { + // prev: [], + // current: [ + // { + // // TODO + // type: 'SetModel', + // model: { + // schema: action.schema, + // relations: action.relations, + // }, + // }, + // ], + // next: [], + // }, + // }; + // } // All actions below are only applicable when diagram is open if (!state) { @@ -128,34 +129,34 @@ export const diagramReducer: Reducer = ( }, }; } - if (isAction(action, DiagramActionTypes.UNDO_EDIT)) { - const newCurrent = state.edits.prev.pop(); - if (!newCurrent) { - return state; - } - return { - ...state, - edits: { - prev: [...state.edits.prev], - current: newCurrent, - next: [...state.edits.next, state.edits.current], - }, - }; - } - if (isAction(action, DiagramActionTypes.REDO_EDIT)) { - const newCurrent = state.edits.next.pop(); - if (!newCurrent) { - return state; - } - return { - ...state, - edits: { - prev: [...state.edits.prev, state.edits.current], - current: newCurrent, - next: [...state.edits.next], - }, - }; - } + // if (isAction(action, DiagramActionTypes.UNDO_EDIT)) { + // const newCurrent = state.edits.prev.pop(); + // if (!newCurrent) { + // return state; + // } + // return { + // ...state, + // edits: { + // prev: [...state.edits.prev], + // current: newCurrent, + // next: [...state.edits.next, state.edits.current], + // }, + // }; + // } + // if (isAction(action, DiagramActionTypes.REDO_EDIT)) { + // const newCurrent = state.edits.next.pop(); + // if (!newCurrent) { + // return state; + // } + // return { + // ...state, + // edits: { + // prev: [...state.edits.prev, state.edits.current], + // current: newCurrent, + // next: [...state.edits.next], + // }, + // }; + // } return state; }; From 2cfef3aa35d1c8deeb1a72f4ca2190435ee739e9 Mon Sep 17 00:00:00 2001 From: Paula Stachova Date: Mon, 19 May 2025 13:49:10 +0200 Subject: [PATCH 03/17] wip --- .../src/services/data-model-storage.ts | 2 +- .../src/store/analysis-process.ts | 14 +- .../src/store/diagram.ts | 144 ++++++++++-------- 3 files changed, 89 insertions(+), 71 deletions(-) diff --git a/packages/compass-data-modeling/src/services/data-model-storage.ts b/packages/compass-data-modeling/src/services/data-model-storage.ts index 918f55e4011..0bd26173f13 100644 --- a/packages/compass-data-modeling/src/services/data-model-storage.ts +++ b/packages/compass-data-modeling/src/services/data-model-storage.ts @@ -36,7 +36,7 @@ export const EditSchema = z.discriminatedUnion('type', [ type: z.literal('SetModel'), id: z.string(), timestamp: z.string(), - model: z.unknown(), // TODO: StaticModelSchema, + model: StaticModelSchema, }), z.object({ type: z.literal('AddRelationship'), diff --git a/packages/compass-data-modeling/src/store/analysis-process.ts b/packages/compass-data-modeling/src/store/analysis-process.ts index d151e45c753..31820d26792 100644 --- a/packages/compass-data-modeling/src/store/analysis-process.ts +++ b/packages/compass-data-modeling/src/store/analysis-process.ts @@ -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: @@ -61,9 +62,8 @@ export type AnalysisFinishedAction = { type: AnalysisProcessActionTypes.ANALYSIS_FINISHED; name: string; connectionId: string; - // TODO - schema: Record; - relations: unknown[]; + collections: { ns: string; schema: MongoDBJSONSchema }[]; + relations: Relationship[]; }; export type AnalysisFailedAction = { @@ -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 = dataService.sampleCursor( ns, @@ -188,7 +188,7 @@ export function startAnalysis( type: AnalysisProcessActionTypes.NAMESPACE_SCHEMA_ANALYZED, namespace: ns, }); - return [ns, schema]; + return { ns, schema }; }) ); if (options.automaticallyInferRelations) { @@ -201,7 +201,7 @@ export function startAnalysis( type: AnalysisProcessActionTypes.ANALYSIS_FINISHED, name, connectionId, - schema: Object.fromEntries(schema), + collections, relations: [], }); void services.dataModelStorage.save( diff --git a/packages/compass-data-modeling/src/store/diagram.ts b/packages/compass-data-modeling/src/store/diagram.ts index ba941f98726..bd5a863d692 100644 --- a/packages/compass-data-modeling/src/store/diagram.ts +++ b/packages/compass-data-modeling/src/store/diagram.ts @@ -4,6 +4,7 @@ import { isAction } from './util'; import type { Edit, MongoDBDataModelDescription, + StaticModel, } from '../services/data-model-storage'; import { AnalysisProcessActionTypes } from './analysis-process'; import { memoize } from 'lodash'; @@ -13,9 +14,9 @@ import { showConfirmation, showPrompt } from '@mongodb-js/compass-components'; export type DiagramState = | (Omit & { edits: { - prev: Edit[]; + prev: Edit[][]; current: Edit[]; - next: Edit[]; + next: Edit[][]; }; }) | null; // null when no diagram is currently open @@ -47,8 +48,7 @@ export type RenameDiagramAction = { export type ApplyEditAction = { type: DiagramActionTypes.APPLY_EDIT; - // TODO - edit: unknown; + edit: Edit; }; export type UndoEditAction = { @@ -86,27 +86,35 @@ export const diagramReducer: Reducer = ( }; } - // if (isAction(action, AnalysisProcessActionTypes.ANALYSIS_FINISHED)) { - // return { - // id: new UUID().toString(), - // name: action.name, - // connectionId: action.connectionId, - // edits: { - // prev: [], - // current: [ - // { - // // TODO - // type: 'SetModel', - // model: { - // schema: action.schema, - // relations: action.relations, - // }, - // }, - // ], - // next: [], - // }, - // }; - // } + if (isAction(action, AnalysisProcessActionTypes.ANALYSIS_FINISHED)) { + return { + id: new UUID().toString(), + name: action.name, + connectionId: action.connectionId, + edits: { + prev: [], + current: [ + { + type: 'SetModel', + id: new UUID().toString(), + timestamp: new Date().toISOString(), + model: { + collections: action.collections.map((collection) => ({ + ns: collection.ns, + jsonSchema: collection.schema, + // TODO + indexes: [], + shardKey: undefined, + displayPosition: [0, 0], + })), + relationships: action.relations, + }, + }, + ], + next: [], + }, + }; + } // All actions below are only applicable when diagram is open if (!state) { @@ -120,6 +128,7 @@ export const diagramReducer: Reducer = ( }; } if (isAction(action, DiagramActionTypes.APPLY_EDIT)) { + console.log('Applying edit', action.edit); return { ...state, edits: { @@ -129,34 +138,34 @@ export const diagramReducer: Reducer = ( }, }; } - // if (isAction(action, DiagramActionTypes.UNDO_EDIT)) { - // const newCurrent = state.edits.prev.pop(); - // if (!newCurrent) { - // return state; - // } - // return { - // ...state, - // edits: { - // prev: [...state.edits.prev], - // current: newCurrent, - // next: [...state.edits.next, state.edits.current], - // }, - // }; - // } - // if (isAction(action, DiagramActionTypes.REDO_EDIT)) { - // const newCurrent = state.edits.next.pop(); - // if (!newCurrent) { - // return state; - // } - // return { - // ...state, - // edits: { - // prev: [...state.edits.prev, state.edits.current], - // current: newCurrent, - // next: [...state.edits.next], - // }, - // }; - // } + if (isAction(action, DiagramActionTypes.UNDO_EDIT)) { + const newCurrent = state.edits.prev.pop(); + if (!newCurrent) { + return state; + } + return { + ...state, + edits: { + prev: [...state.edits.prev], + current: newCurrent, + next: [...state.edits.next, state.edits.current], + }, + }; + } + if (isAction(action, DiagramActionTypes.REDO_EDIT)) { + const newCurrent = state.edits.next.pop(); + if (!newCurrent) { + return state; + } + return { + ...state, + edits: { + prev: [...state.edits.prev, state.edits.current], + current: newCurrent, + next: [...state.edits.next], + }, + }; + } return state; }; @@ -175,7 +184,7 @@ export function redoEdit(): DataModelingThunkAction { } export function applyEdit( - edit: unknown + edit: Edit ): DataModelingThunkAction { return (dispatch, getState, { dataModelStorage }) => { dispatch({ type: DiagramActionTypes.APPLY_EDIT, edit }); @@ -230,20 +239,29 @@ export function renameDiagram( }; } -// TODO -function _applyEdit(model: any, edit: any) { - if (edit && 'type' in edit) { - if (edit.type === 'SetModel') { - return edit.model; - } +function _applyEdit(edit: Edit, model?: StaticModel): StaticModel { + if (edit.type === 'SetModel') { + return edit.model; + } + if (!model) { + throw new Error('Editing a model that has not been initialized'); + } + if (edit.type === 'AddRelationship') { + return { + ...model, + relationships: [...model.relationships, edit.relationship], + }; } return model; } -export function getCurrentModel(diagram: MongoDBDataModelDescription): unknown { - let model; +export function getCurrentModel( + diagram: MongoDBDataModelDescription +): StaticModel { + let model: StaticModel; for (const edit of diagram.edits) { - model = _applyEdit(model, edit); + console.log('Applying edit', edit, diagram); + model = _applyEdit(edit, model); } return model; } From ab816be74d1edffd3fa969651ae5966035ca0e5a Mon Sep 17 00:00:00 2001 From: Paula Stachova Date: Mon, 19 May 2025 15:24:34 +0200 Subject: [PATCH 04/17] feat: diagram edit handling COMPASS-9312 --- .../src/components/diagram-editor.tsx | 68 ++++++++++++++++++- .../src/services/data-model-storage.ts | 1 + .../src/store/diagram.ts | 67 ++++++++++++++---- 3 files changed, 120 insertions(+), 16 deletions(-) diff --git a/packages/compass-data-modeling/src/components/diagram-editor.tsx b/packages/compass-data-modeling/src/components/diagram-editor.tsx index a0a60db4a59..3b718a2558c 100644 --- a/packages/compass-data-modeling/src/components/diagram-editor.tsx +++ b/packages/compass-data-modeling/src/components/diagram-editor.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { connect } from 'react-redux'; import type { DataModelingState } from '../store/reducer'; import { CodemirrorMultilineEditor } from '@mongodb-js/compass-editor'; @@ -19,6 +19,9 @@ import { spacing, Button, palette, + SplitButton, + RadioBoxGroup, + RadioBox, } from '@mongodb-js/compass-components'; import { cancelAnalysis, retryAnalysis } from '../store/analysis-process'; @@ -73,7 +76,7 @@ const modelPreviewStyles = css({ }); const editorContainerStyles = css({ - height: 160 + 34 + 16, + height: 46 + 160 + 34 + 16, display: 'flex', flexDirection: 'column', gap: 8, @@ -86,6 +89,15 @@ const editorContainerApplyButtonStyles = css({ alignSelf: 'flex-end', }); +const editorContainerPlaceholderButtonStyles = css({ + paddingLeft: 8, + paddingRight: 8, + alignSelf: 'flex-start', + display: 'flex', + gap: spacing[200], + paddingTop: spacing[200], +}); + const DiagramEditor: React.FunctionComponent<{ step: DataModelingState['step']; hasUndo: boolean; @@ -118,6 +130,44 @@ const DiagramEditor: React.FunctionComponent<{ } }, [applyInput]); + const applyPlaceholder = useCallback( + (type: 'AddRelationship' | 'RemoveRelationship') => () => { + let placeholder = {}; + switch (type) { + case 'AddRelationship': + placeholder = { + type: 'AddRelationship', + 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', + id: 'relationship1', + relationshipId: 'relationship1', + }; + break; + default: + throw new Error(`Unknown placeholder ${placeholder}`); + } + setApplyInput(JSON.stringify(placeholder, null, 2)); + }, + [setApplyInput] + ); + const modelStr = useMemo(() => { return JSON.stringify(model, null, 2); }, [model]); @@ -172,6 +222,20 @@ const DiagramEditor: React.FunctionComponent<{ >
+
+ + +
= ( }; } if (isAction(action, DiagramActionTypes.APPLY_EDIT)) { - console.log('Applying edit', action.edit); return { ...state, edits: { @@ -187,7 +186,14 @@ export function applyEdit( edit: Edit ): DataModelingThunkAction { return (dispatch, getState, { dataModelStorage }) => { - dispatch({ type: DiagramActionTypes.APPLY_EDIT, edit }); + dispatch({ + type: DiagramActionTypes.APPLY_EDIT, + edit: { + ...edit, + id: new UUID().toString(), + timestamp: new Date().toISOString(), + }, + }); void dataModelStorage.save(getCurrentDiagramFromState(getState())); }; } @@ -246,24 +252,57 @@ function _applyEdit(edit: Edit, model?: StaticModel): StaticModel { if (!model) { throw new Error('Editing a model that has not been initialized'); } - if (edit.type === 'AddRelationship') { - return { - ...model, - relationships: [...model.relationships, edit.relationship], - }; + switch (edit.type) { + case 'AddRelationship': { + return { + ...model, + relationships: [...model.relationships, edit.relationship], + }; + } + case 'RemoveRelationship': { + return { + ...model, + relationships: model.relationships.filter( + (relationship) => relationship.id !== edit.relationshipId + ), + }; + } + default: { + return model; + } } - return model; } export function getCurrentModel( - diagram: MongoDBDataModelDescription + description: MongoDBDataModelDescription ): StaticModel { - let model: StaticModel; - for (const edit of diagram.edits) { - console.log('Applying edit', edit, diagram); - model = _applyEdit(edit, model); + // Get the last 'SetModel' edit. + const reversedSetModelEditIndex = description.edits + .slice() + .reverse() + .findIndex((edit) => edit.type === 'SetModel'); + if (reversedSetModelEditIndex === -1) { + throw new Error('No diagram model found.'); + } + + // Calculate the actual index in the original array. + const lastSetModelEditIndex = + description.edits.length - 1 - reversedSetModelEditIndex; + + // Start with the StaticModel from the last `SetModel` edit. + if (description.edits[lastSetModelEditIndex].type !== 'SetModel') { + throw new Error('Something went wrong, last edit is not a SetModel'); } - return model; + let currentModel: StaticModel = + description.edits[lastSetModelEditIndex].model; + + // Apply all subsequent edits after the last `SetModel` edit. + for (let i = lastSetModelEditIndex + 1; i < description.edits.length; i++) { + const edit = description.edits[i]; + currentModel = _applyEdit(edit, currentModel); + } + + return currentModel; } export function getCurrentDiagramFromState( From 298348d3971f841eb4743f328d4eef6ffc2b0418 Mon Sep 17 00:00:00 2001 From: Paula Stachova Date: Mon, 19 May 2025 17:10:02 +0200 Subject: [PATCH 05/17] add validation --- package-lock.json | 22 +++++++- packages/compass-data-modeling/package.json | 3 +- .../src/components/diagram-editor.tsx | 56 ++++++++++--------- .../src/services/data-model-storage.ts | 19 +++++++ .../src/store/diagram.ts | 47 ++++++++++++---- 5 files changed, 108 insertions(+), 39 deletions(-) diff --git a/package-lock.json b/package-lock.json index 310ffed52de..d4edb3dbf16 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44360,7 +44360,8 @@ "react-dom": "^17.0.2", "sinon": "^17.0.1", "typescript": "^5.0.4", - "xvfb-maybe": "^0.2.1" + "xvfb-maybe": "^0.2.1", + "zod": "^3.25.1" } }, "packages/compass-data-modeling/node_modules/@sinonjs/commons": { @@ -44452,6 +44453,16 @@ "url": "https://opencollective.com/sinon" } }, + "packages/compass-data-modeling/node_modules/zod": { + "version": "3.25.1", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.1.tgz", + "integrity": "sha512-bkxUGQiqWDTXHSgqtevYDri5ee2GPC9szPct4pqpzLEpswgDQmuseDz81ZF0AnNu1xsmnBVmbtv/t/WeUIHlpg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "packages/compass-database": { "name": "@mongodb-js/compass-database", "version": "3.19.1", @@ -56868,7 +56879,8 @@ "redux-thunk": "^2.4.2", "sinon": "^17.0.1", "typescript": "^5.0.4", - "xvfb-maybe": "^0.2.1" + "xvfb-maybe": "^0.2.1", + "zod": "^3.25.1" }, "dependencies": { "@sinonjs/commons": { @@ -56946,6 +56958,12 @@ "nise": "^5.1.5", "supports-color": "^7.2.0" } + }, + "zod": { + "version": "3.25.1", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.1.tgz", + "integrity": "sha512-bkxUGQiqWDTXHSgqtevYDri5ee2GPC9szPct4pqpzLEpswgDQmuseDz81ZF0AnNu1xsmnBVmbtv/t/WeUIHlpg==", + "dev": true } } }, diff --git a/packages/compass-data-modeling/package.json b/packages/compass-data-modeling/package.json index b1312ead9ad..8a4229d6995 100644 --- a/packages/compass-data-modeling/package.json +++ b/packages/compass-data-modeling/package.json @@ -93,7 +93,8 @@ "react-dom": "^17.0.2", "sinon": "^17.0.1", "typescript": "^5.0.4", - "xvfb-maybe": "^0.2.1" + "xvfb-maybe": "^0.2.1", + "zod": "^3.25.1" }, "is_compass_plugin": true } diff --git a/packages/compass-data-modeling/src/components/diagram-editor.tsx b/packages/compass-data-modeling/src/components/diagram-editor.tsx index 3b718a2558c..a5a026aaed4 100644 --- a/packages/compass-data-modeling/src/components/diagram-editor.tsx +++ b/packages/compass-data-modeling/src/components/diagram-editor.tsx @@ -19,11 +19,10 @@ import { spacing, Button, palette, - SplitButton, - RadioBoxGroup, - RadioBox, + 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%', @@ -76,17 +75,20 @@ const modelPreviewStyles = css({ }); const editorContainerStyles = css({ - height: 46 + 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[1200], }); const editorContainerPlaceholderButtonStyles = css({ @@ -104,11 +106,11 @@ 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, @@ -116,6 +118,7 @@ const DiagramEditor: React.FunctionComponent<{ hasRedo, onRedoClick, model, + editErrors, onRetryClick, onCancelClick, onApplyClick, @@ -137,26 +140,27 @@ const DiagramEditor: React.FunctionComponent<{ case 'AddRelationship': placeholder = { type: 'AddRelationship', - id: 'relationship1', - relationship: [ - { - ns: 'db.sourceCollection', - cardinality: 1, - fields: ['field1'], - }, - { - ns: 'db.targetCollection', - cardinality: 1, - fields: ['field2'], - }, - ], - isInferred: false, + 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', - id: 'relationship1', relationshipId: 'relationship1', }; break; @@ -244,7 +248,8 @@ const DiagramEditor: React.FunctionComponent<{ maxLines={10} >
-
+
+ {editErrors && }