Skip to content

Commit 537b4a7

Browse files
committed
feat(compass-data-modeling): update field type COMPASS-9659
1 parent 48cc245 commit 537b4a7

File tree

4 files changed

+205
-26
lines changed

4 files changed

+205
-26
lines changed

packages/compass-data-modeling/src/components/drawer/field-drawer-content.tsx

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
} from '@mongodb-js/compass-components';
1212
import { BSONType } from 'mongodb';
1313
import {
14+
changeFieldType,
1415
createNewRelationship,
1516
deleteRelationship,
1617
getCurrentDiagramFromState,
@@ -56,8 +57,7 @@ type FieldDrawerContentProps = {
5657
onChangeFieldType: (
5758
namespace: string,
5859
fieldPath: FieldPath,
59-
fromBsonType: string | string[],
60-
toBsonType: string | string[]
60+
newTypes: string[]
6161
) => void;
6262
};
6363

@@ -137,17 +137,19 @@ const FieldDrawerContent: React.FunctionComponent<FieldDrawerContentProps> = ({
137137
[fieldPath, fieldPaths, fieldName]
138138
);
139139

140-
const handleTypeChange = (newTypes: string | string[]) => {
141-
onChangeFieldType(namespace, fieldPath, types, newTypes);
140+
const handleTypeChange = (newTypes: string[]) => {
141+
onChangeFieldType(namespace, fieldPath, newTypes);
142142
};
143143

144+
const isReadOnly = useMemo(() => isIdField(fieldPath), [fieldPath]);
145+
144146
return (
145147
<>
146148
<DMDrawerSection label="Field properties">
147149
<DMFormFieldContainer>
148150
<TextInput
149151
label="Field name"
150-
disabled={isIdField(fieldPath)}
152+
disabled={isReadOnly}
151153
data-testid="data-model-collection-drawer-name-input"
152154
sizeVariant="small"
153155
value={fieldName}
@@ -162,7 +164,7 @@ const FieldDrawerContent: React.FunctionComponent<FieldDrawerContentProps> = ({
162164
data-testid="lg-combobox-datatype"
163165
label="Datatype"
164166
aria-label="Datatype"
165-
disabled={true} // TODO(COMPASS-9659): enable when field type change is implemented
167+
disabled={isReadOnly}
166168
value={types}
167169
size="small"
168170
multiselect={true}
@@ -228,6 +230,6 @@ export default connect(
228230
onEditRelationshipClick: selectRelationship,
229231
onDeleteRelationshipClick: deleteRelationship,
230232
onRenameField: renameField,
231-
onChangeFieldType: () => {}, // TODO(COMPASS-9659): updateFieldSchema,
233+
onChangeFieldType: changeFieldType,
232234
}
233235
)(FieldDrawerContent);

packages/compass-data-modeling/src/store/apply-edit.ts

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ export function applyEdit(edit: Edit, model?: StaticModel): StaticModel {
167167
jsonSchema: updateSchema({
168168
jsonSchema: collection.jsonSchema,
169169
fieldPath: edit.field,
170-
update: 'removeField',
170+
updateParameters: { update: 'removeField' },
171171
}),
172172
};
173173
}),
@@ -198,8 +198,29 @@ export function applyEdit(edit: Edit, model?: StaticModel): StaticModel {
198198
jsonSchema: updateSchema({
199199
jsonSchema: collection.jsonSchema,
200200
fieldPath: edit.field,
201-
update: 'renameField',
202-
newFieldName: edit.newName,
201+
updateParameters: {
202+
update: 'renameField',
203+
newFieldName: edit.newName,
204+
},
205+
}),
206+
};
207+
}),
208+
};
209+
}
210+
case 'ChangeFieldType': {
211+
return {
212+
...model,
213+
collections: model.collections.map((collection) => {
214+
if (collection.ns !== edit.ns) return collection;
215+
return {
216+
...collection,
217+
jsonSchema: updateSchema({
218+
jsonSchema: collection.jsonSchema,
219+
fieldPath: edit.field,
220+
updateParameters: {
221+
update: 'changeFieldSchema',
222+
newFieldSchema: edit.to,
223+
},
203224
}),
204225
};
205226
}),

packages/compass-data-modeling/src/store/diagram.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,11 @@ import type { MongoDBJSONSchema } from 'mongodb-schema';
2929
import { getCoordinatesForNewNode } from '@mongodb-js/diagramming';
3030
import { collectionToDiagramNode } from '../utils/nodes-and-edges';
3131
import toNS from 'mongodb-ns';
32-
import { traverseSchema } from '../utils/schema-traversal';
32+
import {
33+
getFieldFromSchema,
34+
getSchemaForNewTypes,
35+
traverseSchema,
36+
} from '../utils/schema-traversal';
3337
import { applyEdit as _applyEdit } from './apply-edit';
3438

3539
function isNonEmptyArray<T>(arr: T[]): arr is [T, ...T[]] {
@@ -684,6 +688,34 @@ export function renameField(
684688
return applyEdit({ type: 'RenameField', ns, field, newName });
685689
}
686690

691+
export function changeFieldType(
692+
ns: string,
693+
fieldPath: FieldPath,
694+
newTypes: string[]
695+
): DataModelingThunkAction<void, ApplyEditAction | ApplyEditFailedAction> {
696+
return (dispatch, getState) => {
697+
const collectionSchema = selectCurrentModelFromState(
698+
getState()
699+
).collections.find((collection) => collection.ns === ns)?.jsonSchema;
700+
if (!collectionSchema) throw new Error('Collection not found in model');
701+
const field = getFieldFromSchema({
702+
jsonSchema: collectionSchema,
703+
fieldPath: fieldPath,
704+
});
705+
if (!field) throw new Error('Field not found in schema');
706+
const to = getSchemaForNewTypes(field, newTypes);
707+
dispatch(
708+
applyEdit({
709+
type: 'ChangeFieldType',
710+
ns,
711+
field: fieldPath,
712+
from: field.jsonSchema,
713+
to,
714+
})
715+
);
716+
};
717+
}
718+
687719
function getPositionForNewCollection(
688720
existingCollections: DataModelCollection[],
689721
newCollection: Omit<DataModelCollection, 'displayPosition'>

packages/compass-data-modeling/src/utils/schema-traversal.tsx

Lines changed: 139 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { MongoDBJSONSchema } from 'mongodb-schema';
1+
import type { JSONSchema, MongoDBJSONSchema } from 'mongodb-schema';
22
import type { FieldPath } from '../services/data-model-storage';
33

44
/**
@@ -167,14 +167,16 @@ export const getFieldFromSchema = ({
167167
};
168168

169169
type UpdateOperationParameters = {
170-
update: 'removeField' | 'renameField';
170+
update: 'removeField' | 'renameField' | 'changeFieldSchema';
171171
newFieldName?: string;
172+
newFieldSchema?: MongoDBJSONSchema;
172173
};
173174

174175
const applySchemaUpdate = ({
175176
schema,
176177
fieldName,
177178
newFieldName,
179+
newFieldSchema,
178180
update,
179181
}: {
180182
schema: MongoDBJSONSchema;
@@ -205,6 +207,22 @@ const applySchemaUpdate = ({
205207
),
206208
};
207209
}
210+
case 'changeFieldSchema': {
211+
if (!schema.properties || !schema.properties[fieldName])
212+
throw new Error('Field to change type does not exist');
213+
if (!newFieldSchema)
214+
throw new Error(
215+
'New field schema is required for the change operation'
216+
);
217+
return {
218+
...schema,
219+
properties: Object.fromEntries(
220+
Object.entries(schema.properties).map(([key, value]) =>
221+
key === fieldName ? [key, newFieldSchema] : [key, value]
222+
)
223+
),
224+
};
225+
}
208226
default:
209227
return schema;
210228
}
@@ -216,12 +234,12 @@ const applySchemaUpdate = ({
216234
export const updateSchema = ({
217235
jsonSchema,
218236
fieldPath,
219-
update,
220-
newFieldName,
237+
updateParameters,
221238
}: {
222239
jsonSchema: MongoDBJSONSchema;
223240
fieldPath: FieldPath;
224-
} & UpdateOperationParameters): MongoDBJSONSchema => {
241+
updateParameters: UpdateOperationParameters;
242+
}): MongoDBJSONSchema => {
225243
const newSchema = {
226244
...jsonSchema,
227245
};
@@ -234,17 +252,15 @@ export const updateSchema = ({
234252
return applySchemaUpdate({
235253
schema: newSchema,
236254
fieldName: nextInPath,
237-
update,
238-
newFieldName,
255+
...updateParameters,
239256
});
240257
}
241258
newSchema.properties = {
242259
...newSchema.properties,
243260
[nextInPath]: updateSchema({
244261
jsonSchema: newSchema.properties[nextInPath],
245262
fieldPath: remainingFieldPath,
246-
update,
247-
newFieldName,
263+
updateParameters,
248264
}),
249265
};
250266
}
@@ -253,8 +269,7 @@ export const updateSchema = ({
253269
updateSchema({
254270
jsonSchema: variant,
255271
fieldPath: fieldPath,
256-
update,
257-
newFieldName,
272+
updateParameters,
258273
})
259274
);
260275
}
@@ -263,20 +278,129 @@ export const updateSchema = ({
263278
newSchema.items = updateSchema({
264279
jsonSchema: newSchema.items,
265280
fieldPath: fieldPath,
266-
update,
267-
newFieldName,
281+
updateParameters,
268282
});
269283
} else {
270284
newSchema.items = newSchema.items.map((item) =>
271285
updateSchema({
272286
jsonSchema: item,
273287
fieldPath: fieldPath,
274-
update,
275-
newFieldName,
288+
updateParameters,
276289
})
277290
);
278291
}
279292
}
280293

281294
return newSchema;
282295
};
296+
297+
const getMin1ArrayVariants = (oldSchema: JSONSchema) => {
298+
const arrayVariants = oldSchema.anyOf?.filter(
299+
(variant) => variant.bsonType === 'array'
300+
);
301+
if (arrayVariants && arrayVariants.length > 0) {
302+
return arrayVariants as [MongoDBJSONSchema, ...MongoDBJSONSchema[]];
303+
}
304+
return [
305+
{
306+
bsonType: 'array',
307+
items: oldSchema.items || [],
308+
},
309+
];
310+
};
311+
312+
const getMin1ObjectVariants = (
313+
oldSchema: JSONSchema
314+
): [MongoDBJSONSchema, ...MongoDBJSONSchema[]] => {
315+
const objectVariants = oldSchema.anyOf?.filter(
316+
(variant) => variant.bsonType === 'object'
317+
);
318+
if (objectVariants && objectVariants.length > 0) {
319+
return objectVariants as [MongoDBJSONSchema, ...MongoDBJSONSchema[]];
320+
}
321+
return [
322+
{
323+
bsonType: 'object',
324+
items: oldSchema.properties || {},
325+
required: oldSchema.required || [],
326+
},
327+
];
328+
};
329+
330+
const getOtherVariants = (
331+
oldSchema: JSONSchema,
332+
newTypes: string[]
333+
): MongoDBJSONSchema[] => {
334+
const existingAnyOfVariants =
335+
oldSchema.anyOf?.filter(
336+
(variant) => variant.bsonType !== 'object' && variant.bsonType !== 'array'
337+
) || [];
338+
const existingAnyOfTypes = existingAnyOfVariants
339+
.map((v) => v.bsonType)
340+
.flat();
341+
const existingBasicTypes = oldSchema.bsonType
342+
? []
343+
: Array.isArray(oldSchema.bsonType)
344+
? oldSchema.bsonType
345+
: [oldSchema.bsonType];
346+
const existingBasicVariants = existingBasicTypes
347+
.filter(
348+
(type) => newTypes.includes(type) && type !== 'object' && type !== 'array'
349+
)
350+
.map((type) => ({ bsonType: type }));
351+
const newVariants = newTypes
352+
.filter(
353+
(type) =>
354+
type !== 'object' &&
355+
type !== 'array' &&
356+
!existingAnyOfTypes.includes(type) &&
357+
!existingBasicTypes.includes(type)
358+
)
359+
.map((type) => ({ bsonType: type }));
360+
return [...existingAnyOfVariants, ...existingBasicVariants, ...newVariants];
361+
};
362+
363+
export function getSchemaForNewTypes(
364+
field: {
365+
fieldTypes: string[];
366+
jsonSchema: MongoDBJSONSchema;
367+
},
368+
newTypes: string[]
369+
): MongoDBJSONSchema {
370+
const { fieldTypes: oldTypes, jsonSchema: oldSchema } = field;
371+
if (oldTypes.join(',') === newTypes.join(',')) return oldSchema;
372+
const newSchema: MongoDBJSONSchema = { ...oldSchema };
373+
374+
// Simple schema - new type does includes neither object nor array
375+
if (!newTypes.some((t) => t === 'object' || t === 'array')) {
376+
newSchema.bsonType = newTypes;
377+
delete newSchema.anyOf;
378+
delete newSchema.properties;
379+
delete newSchema.items;
380+
delete newSchema.required;
381+
return newSchema;
382+
}
383+
384+
// Complex schema
385+
386+
// We collect array sub-schemas we need to keep
387+
// Then we add new sub-schemas if needed
388+
const arraySchemas: MongoDBJSONSchema[] = newTypes.includes('array')
389+
? getMin1ArrayVariants(oldSchema)
390+
: [];
391+
392+
// We collect object sub-schemas we need to keep
393+
const objectSchemas: MongoDBJSONSchema[] = newTypes.includes('object')
394+
? getMin1ObjectVariants(oldSchema)
395+
: [];
396+
397+
const otherSchemas: MongoDBJSONSchema[] = getOtherVariants(
398+
oldSchema,
399+
newTypes
400+
);
401+
402+
// Finally we set the anyOf to the collected sub-schemas
403+
newSchema.anyOf = [...arraySchemas, ...objectSchemas, ...otherSchemas];
404+
405+
return newSchema;
406+
}

0 commit comments

Comments
 (0)