Skip to content

Commit 49134d1

Browse files
committed
feat: internal to Standard JSON Schema conversion COMPASS-8700
1 parent 530563f commit 49134d1

File tree

1 file changed

+314
-6
lines changed

1 file changed

+314
-6
lines changed
Lines changed: 314 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,320 @@
1-
import { InternalSchema } from '..';
1+
import { ArraySchemaType, DocumentSchemaType, Schema as InternalSchema, SchemaType } from '../schema-analyzer';
22
import { StandardJSONSchema } from '../types';
33

4-
export default function internalSchemaToStandard(
5-
/* eslint @typescript-eslint/no-unused-vars: 0 */
4+
const InternalTypeToStandardTypeMap: Record<
5+
SchemaType['name'] | 'Double' | 'BSONSymbol',
6+
string | { $ref: string }
7+
> = {
8+
Double: { $ref: '#/$defs/Double' },
9+
Number: { $ref: '#/$defs/Double' },
10+
String: 'string',
11+
Document: 'object',
12+
Array: 'array',
13+
Binary: { $ref: '#/$defs/Binary' },
14+
Undefined: { $ref: '#/$defs/Undefined' },
15+
ObjectId: { $ref: '#/$defs/ObjectId' },
16+
Boolean: 'boolean',
17+
Date: { $ref: '#/$defs/Date' },
18+
Null: 'null',
19+
RegExp: { $ref: '#/$defs/RegExp' },
20+
BSONRegExp: { $ref: '#/$defs/RegExp' },
21+
DBRef: { $ref: '#/$defs/DBRef' },
22+
DBPointer: { $ref: '#/$defs/DBPointer' },
23+
BSONSymbol: { $ref: '#/$defs/BSONSymbol' },
24+
Symbol: { $ref: '#/$defs/BSONSymbol' },
25+
Code: { $ref: '#/$defs/Code' },
26+
Int32: 'integer',
27+
Timestamp: { $ref: '#/$defs/Timestamp' },
28+
Long: 'integer',
29+
Decimal128: { $ref: '#/$defs/Decimal' },
30+
MinKey: { $ref: '#/$defs/MinKey' },
31+
MaxKey: { $ref: '#/$defs/MaxKey' }
32+
};
33+
34+
const RELAXED_EJSON_DEFINITIONS = Object.freeze({
35+
ObjectId: {
36+
type: 'object',
37+
properties: {
38+
$oid: {
39+
type: 'string',
40+
pattern: '^[0-9a-fA-F]{24}$'
41+
}
42+
},
43+
required: ['$oid'],
44+
additionalProperties: false
45+
},
46+
BSONSymbol: {
47+
type: 'object',
48+
properties: {
49+
$symbol: {
50+
type: 'string'
51+
}
52+
},
53+
required: ['$symbol'],
54+
additionalProperties: false
55+
},
56+
Double: {
57+
oneOf: [
58+
{ type: 'number' },
59+
{
60+
enum: ['Infinity', '-Infinity', 'NaN']
61+
}
62+
]
63+
},
64+
Decimal128: {
65+
type: 'object',
66+
properties: {
67+
$numberDecimal: {
68+
type: 'string'
69+
}
70+
},
71+
required: ['$numberDecimal'],
72+
additionalProperties: false
73+
},
74+
Binary: {
75+
type: 'object',
76+
properties: {
77+
$binary: {
78+
type: 'object',
79+
properties: {
80+
base64: {
81+
type: 'string'
82+
},
83+
subType: {
84+
type: 'string',
85+
pattern: '^[0-9a-fA-F]{1,2}$' // BSON binary type as a one- or two-character hex string
86+
}
87+
},
88+
required: ['base64', 'subType'],
89+
additionalProperties: false
90+
}
91+
},
92+
required: ['$binary'],
93+
additionalProperties: false
94+
},
95+
Code: {
96+
type: 'object',
97+
properties: {
98+
$code: {
99+
type: 'string'
100+
}
101+
},
102+
required: ['$code'],
103+
additionalProperties: false
104+
},
105+
CodeWScope: {
106+
type: 'object',
107+
properties: {
108+
$code: {
109+
type: 'string'
110+
},
111+
$scope: {
112+
type: 'object' // TODO: object is ejson object hmm
113+
}
114+
},
115+
required: ['$code', '$scope'],
116+
additionalProperties: false
117+
},
118+
Timestamp: {
119+
type: 'object',
120+
properties: {
121+
$timestamp: {
122+
type: 'object',
123+
properties: {
124+
t: {
125+
type: 'integer',
126+
minimum: 0
127+
},
128+
i: {
129+
type: 'integer',
130+
minimum: 0
131+
}
132+
},
133+
required: ['t', 'i'],
134+
additionalProperties: false
135+
}
136+
},
137+
required: ['$timestamp'],
138+
additionalProperties: false
139+
},
140+
RegExp: {
141+
type: 'object',
142+
properties: {
143+
$regularExpression: {
144+
type: 'object',
145+
properties: {
146+
pattern: {
147+
type: 'string'
148+
},
149+
options: {
150+
type: 'string',
151+
pattern: '^[gimuy]*$'
152+
}
153+
},
154+
required: ['pattern'],
155+
additionalProperties: false
156+
}
157+
},
158+
required: ['$regularExpression'],
159+
additionalProperties: false
160+
},
161+
DBPointer: {
162+
type: 'object',
163+
properties: {
164+
$dbPointer: {
165+
type: 'object',
166+
properties: {
167+
$ref: {
168+
type: 'string'
169+
},
170+
$id: {
171+
$ref: '#/$defs/Decimal'
172+
}
173+
},
174+
required: ['$ref', '$id'],
175+
additionalProperties: false
176+
}
177+
},
178+
required: ['$dbPointer'],
179+
additionalProperties: false
180+
},
181+
Date: {
182+
type: 'object',
183+
properties: {
184+
$date: {
185+
type: 'string',
186+
format: 'date-time'
187+
}
188+
},
189+
required: ['$date'],
190+
additionalProperties: false
191+
},
192+
DBRef: {
193+
type: 'object',
194+
properties: {
195+
$ref: {
196+
type: 'string'
197+
},
198+
$id: {},
199+
$db: {
200+
type: 'string'
201+
}
202+
},
203+
required: ['$ref', '$id'],
204+
additionalProperties: true
205+
},
206+
MinKey: {
207+
type: 'object',
208+
properties: {
209+
$minKey: {
210+
type: 'integer',
211+
const: 1
212+
}
213+
},
214+
required: ['$minKey'],
215+
additionalProperties: false
216+
},
217+
MaxKey: {
218+
type: 'object',
219+
properties: {
220+
$maxKey: {
221+
type: 'integer',
222+
const: 1
223+
}
224+
},
225+
required: ['$maxKey'],
226+
additionalProperties: false
227+
},
228+
Undefined: {
229+
type: 'object',
230+
properties: {
231+
$undefined: {
232+
type: 'boolean',
233+
const: true
234+
}
235+
},
236+
required: ['$undefined'],
237+
additionalProperties: false
238+
}
239+
});
240+
241+
const convertInternalType = (type: string) => {
242+
const bsonType = InternalTypeToStandardTypeMap[type];
243+
if (!bsonType) throw new Error(`Encountered unknown type: ${type}`);
244+
return bsonType;
245+
};
246+
247+
async function allowAbort(signal?: AbortSignal) {
248+
return new Promise<void>((resolve, reject) =>
249+
setTimeout(() => {
250+
if (signal?.aborted) return reject(signal?.reason || new Error('Operation aborted'));
251+
resolve();
252+
})
253+
);
254+
}
255+
256+
async function parseType(type: SchemaType, signal?: AbortSignal): Promise<StandardJSONSchema> {
257+
await allowAbort(signal);
258+
const schema: StandardJSONSchema = {
259+
bsonType: convertInternalType(type.bsonType)
260+
};
261+
switch (type.bsonType) {
262+
case 'Array':
263+
schema.items = await parseTypes((type as ArraySchemaType).types);
264+
break;
265+
case 'Document':
266+
Object.assign(schema,
267+
await parseFields((type as DocumentSchemaType).fields, signal)
268+
);
269+
break;
270+
}
271+
272+
return schema;
273+
}
274+
275+
async function parseTypes(types: SchemaType[], signal?: AbortSignal): Promise<StandardJSONSchema> {
276+
await allowAbort(signal);
277+
const definedTypes = types.filter(type => type.bsonType.toLowerCase() !== 'undefined');
278+
const isSingleType = definedTypes.length === 1;
279+
if (isSingleType) {
280+
return parseType(definedTypes[0], signal);
281+
}
282+
const parsedTypes = await Promise.all(definedTypes.map(type => parseType(type, signal)));
283+
if (definedTypes.some(type => ['Document', 'Array'].includes(type.bsonType))) {
284+
return {
285+
anyOf: parsedTypes
286+
};
287+
}
288+
return {
289+
bsonType: definedTypes.map((type) => convertInternalType(type.bsonType))
290+
};
291+
}
292+
293+
async function parseFields(fields: DocumentSchemaType['fields'], signal?: AbortSignal): Promise<{
294+
required: StandardJSONSchema['required'],
295+
properties: StandardJSONSchema['properties'],
296+
}> {
297+
const required = [];
298+
const properties: StandardJSONSchema['properties'] = {};
299+
for (const field of fields) {
300+
if (field.probability === 1) required.push(field.name);
301+
properties[field.name] = await parseTypes(field.types, signal);
302+
}
303+
304+
return { required, properties };
305+
}
306+
307+
export default async function internalSchemaToMongodb(
6308
internalSchema: InternalSchema,
7309
options: {
8310
signal?: AbortSignal
9-
}): Promise<StandardJSONSchema> {
10-
// TODO: COMPASS-8700
11-
return Promise.resolve({} as StandardJSONSchema);
311+
} = {}): Promise<StandardJSONSchema> {
312+
const { required, properties } = await parseFields(internalSchema.fields, options.signal);
313+
const schema: StandardJSONSchema = {
314+
bsonType: 'object',
315+
required,
316+
properties,
317+
$defs: RELAXED_EJSON_DEFINITIONS
318+
};
319+
return schema;
12320
}

0 commit comments

Comments
 (0)