Skip to content

Commit bb75d86

Browse files
committed
field type
1 parent c2b5cb3 commit bb75d86

File tree

3 files changed

+236
-6
lines changed

3 files changed

+236
-6
lines changed

packages/compass-components/src/components/document-list/element-editors.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { mergeProps } from '../../utils/merge-props';
1010
import { documentTypography } from './typography';
1111
import { Icon, Tooltip } from '../leafygreen';
1212
import { useDarkMode } from '../../hooks/use-theme';
13+
import { lowerFirst } from 'lodash';
1314

1415
const maxWidth = css({
1516
maxWidth: '100%',

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

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@ import React, { useMemo } from 'react';
22
import { connect } from 'react-redux';
33
import type {
44
FieldPath,
5-
FieldSchema,
65
Relationship,
76
} from '../../services/data-model-storage';
8-
import { TextInput } from '@mongodb-js/compass-components';
7+
import {
8+
Combobox,
9+
ComboboxOption,
10+
TextInput,
11+
} from '@mongodb-js/compass-components';
912
import toNS from 'mongodb-ns';
1013
import {
1114
createNewRelationship,
@@ -21,12 +24,14 @@ import {
2124
} from './drawer-section-components';
2225
import { useChangeOnBlur } from './use-change-on-blur';
2326
import { RelationshipsSection } from './relationships-section';
27+
import { getFieldFromSchema } from '../../utils/schema-traversal';
28+
import { lowerFirst } from 'lodash';
2429

2530
type FieldDrawerContentProps = {
2631
namespace: string;
2732
fieldPath: FieldPath;
2833
fieldPaths: FieldPath[];
29-
jsonSchema: FieldSchema;
34+
types: string[];
3035
relationships: Relationship[];
3136
onCreateNewRelationshipClick: ({
3237
localNamespace,
@@ -50,6 +55,17 @@ type FieldDrawerContentProps = {
5055
) => void;
5156
};
5257

58+
const TYPES = [
59+
'objectId',
60+
'string',
61+
'int',
62+
'bool',
63+
'date',
64+
'object',
65+
'array',
66+
] as const;
67+
// TODO: get the full list from somewhere
68+
5369
export function getIsFieldNameValid(
5470
currentFieldPath: FieldPath,
5571
existingFields: FieldPath[],
@@ -87,14 +103,15 @@ const FieldDrawerContent: React.FunctionComponent<FieldDrawerContentProps> = ({
87103
namespace,
88104
fieldPath,
89105
fieldPaths,
90-
jsonSchema,
106+
types,
91107
relationships,
92108
onCreateNewRelationshipClick,
93109
onEditRelationshipClick,
94110
onDeleteRelationshipClick,
95111
onRenameField,
96112
onChangeFieldType,
97113
}) => {
114+
console.log({ TYPES, types });
98115
const { value: fieldName, ...nameInputProps } = useChangeOnBlur(
99116
fieldPath[fieldPath.length - 1],
100117
(fieldName) => {
@@ -133,6 +150,24 @@ const FieldDrawerContent: React.FunctionComponent<FieldDrawerContentProps> = ({
133150
errorMessage={fieldNameEditErrorMessage}
134151
/>
135152
</DMFormFieldContainer>
153+
154+
<DMFormFieldContainer>
155+
<Combobox
156+
aria-label="Datatype"
157+
disabled={true} // TODO: enable when field type change is implemented
158+
value={types}
159+
multiselect={true}
160+
clearable={false}
161+
>
162+
{TYPES.map((type) => (
163+
<ComboboxOption
164+
key={type}
165+
value={lowerFirst(type)}
166+
displayName={type}
167+
/>
168+
))}
169+
</Combobox>
170+
</DMFormFieldContainer>
136171
</DMDrawerSection>
137172

138173
<RelationshipsSection
@@ -169,8 +204,15 @@ export default connect(
169204
) => {
170205
const model = selectCurrentModelFromState(state);
171206
return {
172-
jsonSchema: {}, // TODO get field schema
173-
fieldPaths: [], // TODO get field paths
207+
types:
208+
getFieldFromSchema({
209+
jsonSchema:
210+
model.collections.find(
211+
(collection) => collection.ns === ownProps.namespace
212+
)?.jsonSchema ?? {},
213+
fieldPath: ownProps.fieldPath,
214+
})?.fieldTypes ?? [],
215+
fieldPaths: [], // TODO get existing field paths
174216
relationships: model.relationships.filter((r) => {
175217
const [local, foreign] = r.relationship;
176218
return (
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import type { MongoDBJSONSchema } from 'mongodb-schema';
2+
import type { FieldPath } from '../services/data-model-storage';
3+
4+
// TODO: add support for update
5+
// renaming fields, deleting fields, adding fields...
6+
// or maybe we can have two traverses? one for parsing, one for updating?
7+
/**
8+
* Traverses a MongoDB JSON schema and calls the visitor for each field.
9+
*
10+
* @param {Object} params - The parameters object.
11+
* @param {MongoDBJSONSchema} params.jsonSchema - The JSON schema to traverse.
12+
* @param {function} params.visitor - Function called for each field.
13+
* @returns {void}
14+
*/
15+
export const traverseSchema = ({
16+
jsonSchema,
17+
parentFieldPath = [],
18+
visitor,
19+
}: {
20+
jsonSchema: MongoDBJSONSchema;
21+
visitor: ({
22+
fieldPath,
23+
fieldTypes,
24+
}: {
25+
fieldPath: FieldPath;
26+
fieldTypes: string[];
27+
}) => void;
28+
parentFieldPath?: FieldPath;
29+
}): void => {
30+
if (!jsonSchema || !jsonSchema.properties) {
31+
return;
32+
}
33+
for (const [name, field] of Object.entries(jsonSchema.properties)) {
34+
// field has types, properties and (optional) children
35+
// types are either direct, or from anyof
36+
// children are either direct (properties), from anyOf, items or items.anyOf
37+
const types: (string | string[])[] = [];
38+
const children: (MongoDBJSONSchema | MongoDBJSONSchema[])[] = [];
39+
if (field.bsonType) {
40+
types.push(field.bsonType);
41+
}
42+
if (field.properties) {
43+
children.push(field);
44+
}
45+
if (field.items) {
46+
children.push((field.items as MongoDBJSONSchema).anyOf || field.items);
47+
}
48+
if (field.anyOf) {
49+
for (const variant of field.anyOf) {
50+
if (variant.bsonType) {
51+
types.push(variant.bsonType);
52+
}
53+
if (variant.properties) {
54+
children.push(variant);
55+
}
56+
if (variant.items) {
57+
children.push(variant.items);
58+
}
59+
}
60+
}
61+
62+
const newFieldPath = [...parentFieldPath, name];
63+
64+
visitor({
65+
fieldPath: newFieldPath,
66+
fieldTypes: types.flat(),
67+
});
68+
69+
children.flat().forEach((child) =>
70+
traverseSchema({
71+
jsonSchema: child,
72+
visitor,
73+
parentFieldPath: newFieldPath,
74+
})
75+
);
76+
}
77+
};
78+
79+
function searchItemsForChild(
80+
items: MongoDBJSONSchema['items'],
81+
child: string
82+
): MongoDBJSONSchema | undefined {
83+
// When items is an array, this indicates multiple non-complex types
84+
if (!items || Array.isArray(items)) return undefined;
85+
// Nested array - we go deeper
86+
if (items.items) {
87+
const result = searchItemsForChild(items.items, child);
88+
if (result) return result;
89+
}
90+
// Array of single type and that type is an object
91+
if (items.properties && items.properties[child]) {
92+
return items.properties[child];
93+
}
94+
// Array of multiple types, possibly including objects
95+
if (items.anyOf) {
96+
for (const item of items.anyOf) {
97+
if (item.properties && item.properties[child]) {
98+
return item.properties[child];
99+
}
100+
}
101+
}
102+
return undefined;
103+
}
104+
105+
/**
106+
* Finds a single field in a MongoDB JSON schema.
107+
*
108+
* @param {Object} params - The parameters object.
109+
* @param {MongoDBJSONSchema} params.jsonSchema - The JSON schema to traverse.
110+
* @param {FieldPath} params.fieldPath - The field path to find.
111+
* @returns {Object} result - The field information.
112+
* @returns {FieldPath[]} result.fieldPath - The full path to the found field.
113+
* @returns {string[]} result.fieldTypes - Flat list of BSON types for the found field.
114+
* @returns {MongoDBJSONSchema} result.jsonSchema - The JSON schema node for the found field.
115+
116+
*/
117+
export const getFieldFromSchema = ({
118+
jsonSchema,
119+
fieldPath,
120+
parentFieldPath = [],
121+
}: {
122+
jsonSchema: MongoDBJSONSchema;
123+
fieldPath: FieldPath;
124+
parentFieldPath?: FieldPath;
125+
}):
126+
| {
127+
fieldTypes: string[];
128+
jsonSchema: MongoDBJSONSchema;
129+
}
130+
| undefined => {
131+
const nextInPath = fieldPath[0];
132+
const remainingFieldPath = fieldPath.slice(1);
133+
let nextStep: MongoDBJSONSchema | undefined;
134+
if (jsonSchema.properties && jsonSchema.properties[nextInPath]) {
135+
nextStep = jsonSchema.properties[nextInPath];
136+
}
137+
if (!nextStep && jsonSchema.items) {
138+
nextStep = searchItemsForChild(jsonSchema.items, nextInPath);
139+
}
140+
if (!nextStep && jsonSchema.anyOf) {
141+
for (const variant of jsonSchema.anyOf) {
142+
if (variant.properties && variant.properties[nextInPath]) {
143+
nextStep = variant.properties[nextInPath];
144+
break;
145+
}
146+
if (variant.items) {
147+
nextStep = searchItemsForChild(variant.items, nextInPath);
148+
if (nextStep) break;
149+
}
150+
}
151+
}
152+
if (!nextStep) {
153+
throw new Error('Field not found in schema');
154+
}
155+
156+
// Reached the end of path, return the field information
157+
if (fieldPath.length === 1) {
158+
const types: string[] = [];
159+
if (nextStep.bsonType) {
160+
if (Array.isArray(nextStep.bsonType)) {
161+
types.push(...nextStep.bsonType);
162+
} else {
163+
types.push(nextStep.bsonType);
164+
}
165+
}
166+
if (nextStep.anyOf) {
167+
types.push(
168+
...nextStep.anyOf.flatMap((variant) => variant.bsonType || [])
169+
);
170+
}
171+
return {
172+
fieldTypes: types,
173+
jsonSchema: nextStep,
174+
};
175+
}
176+
console.log('Search lower', {
177+
jsonSchema: nextStep,
178+
fieldPath: remainingFieldPath,
179+
parentFieldPath: [...parentFieldPath, nextInPath],
180+
});
181+
// Continue searching in the next step
182+
return getFieldFromSchema({
183+
jsonSchema: nextStep,
184+
fieldPath: remainingFieldPath,
185+
parentFieldPath: [...parentFieldPath, nextInPath],
186+
});
187+
};

0 commit comments

Comments
 (0)