Skip to content

Commit ad08230

Browse files
martin-hasurajulian-mayorga
authored andcommitted
MongoDB: Updated for dynamic $oid and $date objects
PR-URL: hasura/graphql-engine-mono#10323 Co-authored-by: Julian <[email protected]> GitOrigin-RevId: 58ca3685c413c52c334a552f43136a8c243c729a
1 parent 2d2fe93 commit ad08230

File tree

2 files changed

+181
-80
lines changed

2 files changed

+181
-80
lines changed

frontend/libs/console/legacy-ce/src/lib/features/Data/MongoTrackCollection/utils/inferLogicalModel.test.ts

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ import { inferLogicalModels } from './inferLogicalModel';
33
describe('inferLogicalModels', () => {
44
it('returns a logical model', () => {
55
const document = {
6-
_id: '11123-123-123',
6+
_id: {
7+
$oid: '11123-123-123',
8+
},
79
name: 'John',
810
age: 30,
911
isActive: true,
@@ -28,7 +30,9 @@ describe('inferLogicalModels', () => {
2830

2931
it('returns multiple logical models with array', () => {
3032
const document = {
31-
_id: '11123-123-123',
33+
_id: {
34+
$oid: '11123-123-123',
35+
},
3236
name: 'John',
3337
age: 30,
3438
isActive: true,
@@ -108,7 +112,9 @@ describe('inferLogicalModels', () => {
108112

109113
it('returns multiple logical models with object', () => {
110114
const document = {
111-
_id: 'asd',
115+
_id: {
116+
$oid: 'asd',
117+
},
112118
name: 'Stu',
113119
year: 2018,
114120
gpa: 3.5,
@@ -264,4 +270,43 @@ describe('inferLogicalModels', () => {
264270
},
265271
]);
266272
});
273+
274+
it('handles documents with object ids with names other than _id', () => {
275+
const document = {
276+
_id: {
277+
$oid: '5a9427648b0beebeb69579cc',
278+
},
279+
name: 'Andrea Le',
280+
281+
movie_id: {
282+
$oid: '573a1390f29313caabcd418c',
283+
},
284+
text: 'Rem officiis eaque repellendus amet eos doloribus. Porro dolor voluptatum voluptates neque culpa molestias. Voluptate unde nulla temporibus ullam.',
285+
date: {
286+
$date: '2012-03-26T23:20:16.000Z',
287+
},
288+
};
289+
290+
const logicalModels = inferLogicalModels(
291+
'new-documents',
292+
JSON.stringify(document)
293+
);
294+
295+
expect(logicalModels).toEqual([
296+
{
297+
fields: [
298+
{ name: '_id', type: { nullable: false, scalar: 'objectId' } },
299+
{ name: 'name', type: { nullable: false, scalar: 'string' } },
300+
{ name: 'email', type: { nullable: false, scalar: 'string' } },
301+
{
302+
name: 'movie_id',
303+
type: { nullable: false, scalar: 'objectId' },
304+
},
305+
{ name: 'text', type: { nullable: false, scalar: 'string' } },
306+
{ name: 'date', type: { nullable: false, scalar: 'date' } },
307+
],
308+
name: 'newdocuments',
309+
},
310+
]);
311+
});
267312
});

frontend/libs/console/legacy-ce/src/lib/features/Data/MongoTrackCollection/utils/inferLogicalModel.ts

Lines changed: 133 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -32,111 +32,168 @@ const getLogicalModelsFromProperties = (
3232
): LogicalModel[] => {
3333
const logicalModels: LogicalModel[] = [];
3434
const fields: LogicalModelField[] = [];
35-
36-
type ItemSchemaTypes = {
37-
anyOf?: Array<{ type: string }>;
38-
type?: string;
39-
};
40-
const isMixedArray = (itemsSchema: ItemSchemaTypes): boolean => {
41-
if (itemsSchema.anyOf) {
42-
const types = itemsSchema.anyOf.map(subSchema => subSchema.type);
43-
return (
44-
types.includes('object') && !types.every(type => type === 'object')
45-
);
46-
}
47-
return false;
48-
};
49-
5035
for (const [rawFieldName, fieldSchema] of Object.entries(properties)) {
5136
const fieldName = sanitizeGraphQLFieldNames(rawFieldName);
52-
53-
if (fieldName === '_id') {
54-
fields.push({
55-
name: fieldName,
56-
type: {
57-
scalar: 'objectId',
58-
nullable: false,
59-
},
60-
});
61-
continue;
62-
}
63-
6437
const nullable = !requiredProperties.includes(fieldName);
6538
const logicalModelPath = parentName
6639
? `${parentName}_${fieldName}`
6740
: fieldName;
6841

69-
if (fieldSchema.type === 'object') {
70-
if (fieldSchema.properties) {
71-
const newLogicalModels = getLogicalModelsFromProperties(
72-
collectionName,
73-
`${collectionName}_${logicalModelPath}`,
74-
fieldSchema.properties,
75-
fieldSchema.required,
76-
logicalModelPath
77-
);
78-
79-
logicalModels.push(...newLogicalModels);
42+
// Get scalars from MongoDB objectid and date objects
43+
const handleMongoDBFieldTypes = (
44+
properties: ObjectSchema['properties']
45+
): { type: 'objectId' | 'date' | 'string' | 'none'; name?: string } => {
46+
if (!properties) {
47+
return { type: 'string' };
48+
}
49+
if (Object.prototype.hasOwnProperty.call(properties, '$oid')) {
50+
return { type: 'objectId' };
51+
}
52+
if (Object.prototype.hasOwnProperty.call(properties, '$date')) {
53+
return { type: 'date' };
54+
}
55+
return { type: 'none' };
56+
};
8057

58+
if (fieldSchema.type === 'object') {
59+
// Seperate MongoDB objectid and date scalars from logical model objects
60+
const mongoDBFieldType = handleMongoDBFieldTypes(fieldSchema.properties);
61+
if (mongoDBFieldType.type !== 'none') {
8162
fields.push({
8263
name: fieldName,
8364
type: {
84-
logical_model: `${collectionName}_${logicalModelPath}`,
85-
nullable,
86-
},
87-
});
88-
} else {
89-
// Empty object just being casted to `string`
90-
fields.push({
91-
name: fieldName,
92-
type: {
93-
scalar: 'string',
94-
nullable,
65+
scalar: mongoDBFieldType.type,
66+
nullable: false,
9567
},
9668
});
69+
continue;
9770
}
71+
// Make new logical model
72+
const newLogicalModels = getLogicalModelsFromProperties(
73+
collectionName,
74+
`${collectionName}_${logicalModelPath}`,
75+
fieldSchema.properties,
76+
fieldSchema.required,
77+
logicalModelPath
78+
);
79+
logicalModels.push(...newLogicalModels);
80+
fields.push({
81+
name: fieldName,
82+
type: {
83+
logical_model: `${collectionName}_${logicalModelPath}`,
84+
nullable,
85+
},
86+
});
9887
}
9988

10089
if (fieldSchema.type === 'array') {
101-
if (isMixedArray(fieldSchema.items)) {
90+
// Throw error for mixed object / scalar array
91+
// Schema inferer returns anyOf if there are any type conflicts
92+
const hasNestedAnyOf = (function checkNestedAnyOf(obj: any): boolean {
93+
if (typeof obj !== 'object' || obj === null) return false;
94+
if ('anyOf' in obj) return true;
95+
return Object.values(obj).some(
96+
val => typeof val === 'object' && checkNestedAnyOf(val)
97+
);
98+
})(fieldSchema.items);
99+
if (hasNestedAnyOf) {
102100
throw new Error(
103-
`The array for field "${fieldName}" contains both objects and scalars (string, int, etc.). Please check and ensure it only contains one for inference. \n Exact key with issue: "${logicalModelPath}"`
101+
`The array for field "${fieldName}" contains both multiple types (objects, string, int, etc.). Please check and ensure it only contains one for inference. \n Exact key with issue: "${logicalModelPath}"`
104102
);
105103
}
104+
105+
// Array of objects
106106
if (fieldSchema.items.type === 'object') {
107-
const newLogicalModels = getLogicalModelsFromProperties(
108-
collectionName,
109-
`${collectionName}_${logicalModelPath}`,
110-
fieldSchema.items.properties,
111-
fieldSchema.items?.required || [],
112-
logicalModelPath
107+
// Check for special mongo scalars
108+
const mongoDBFieldType = handleMongoDBFieldTypes(
109+
fieldSchema.items.properties
113110
);
111+
if (mongoDBFieldType.type !== 'none') {
112+
fields.push({
113+
name: fieldName,
114+
type: {
115+
array: {
116+
scalar: mongoDBFieldType.type,
117+
nullable,
118+
},
119+
},
120+
});
121+
} else {
122+
// Make new logical model for array
123+
const newLogicalModels = getLogicalModelsFromProperties(
124+
collectionName,
125+
`${collectionName}_${logicalModelPath}`,
126+
fieldSchema.items.properties,
127+
fieldSchema.items?.required || [],
128+
logicalModelPath
129+
);
114130

115-
logicalModels.push(...newLogicalModels);
131+
logicalModels.push(...newLogicalModels);
116132

117-
fields.push({
118-
name: fieldName,
119-
type: {
120-
array: {
121-
logical_model: `${collectionName}_${logicalModelPath}`,
122-
nullable,
133+
fields.push({
134+
name: fieldName,
135+
type: {
136+
array: {
137+
logical_model: `${collectionName}_${logicalModelPath}`,
138+
nullable,
139+
},
123140
},
124-
},
125-
});
126-
} else {
127-
// scalar array
128-
fields.push({
129-
name: fieldName,
130-
type: {
131-
array: {
132-
scalar: fieldSchema.items.type,
133-
nullable,
141+
});
142+
}
143+
continue;
144+
}
145+
// Array of scalars
146+
if (fieldSchema.items.type !== 'object') {
147+
// Process scalar types in array
148+
// TODO: DRY this out with scalar processing below
149+
if (fieldSchema.items.type === 'string') {
150+
fields.push({
151+
name: fieldName,
152+
type: {
153+
array: {
154+
scalar: 'string',
155+
nullable,
156+
},
134157
},
135-
},
136-
});
158+
});
159+
}
160+
if (fieldSchema.items.type === 'integer') {
161+
fields.push({
162+
name: fieldName,
163+
type: {
164+
array: {
165+
scalar: 'int',
166+
nullable,
167+
},
168+
},
169+
});
170+
}
171+
if (fieldSchema.items.type === 'number') {
172+
fields.push({
173+
name: fieldName,
174+
type: {
175+
array: {
176+
scalar: 'double',
177+
nullable,
178+
},
179+
},
180+
});
181+
}
182+
if (fieldSchema.items.type === 'boolean') {
183+
fields.push({
184+
name: fieldName,
185+
type: {
186+
array: {
187+
scalar: 'bool',
188+
nullable,
189+
},
190+
},
191+
});
192+
}
137193
}
138194
}
139195

196+
// Process scalars
140197
if (fieldSchema.type === 'string') {
141198
fields.push({
142199
name: fieldName,
@@ -177,7 +234,6 @@ const getLogicalModelsFromProperties = (
177234
});
178235
}
179236
}
180-
181237
return [
182238
{
183239
name,

0 commit comments

Comments
 (0)