Skip to content

Commit d693c0d

Browse files
authored
fix: limit definitions based on use COMPASS-8941 (#226)
1 parent 95f3787 commit d693c0d

14 files changed

+362
-264
lines changed

src/index.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ import type {
1717
SimplifiedSchemaField,
1818
SimplifiedSchema
1919
} from './schema-analyzer';
20+
import { convertInternalToExpanded } from './schema-converters/internalToExpanded';
21+
import { convertInternalToMongodb } from './schema-converters/internalToMongoDB';
22+
import { convertInternalToStandard } from './schema-converters/internalToStandard';
2023
import * as schemaStats from './stats';
2124
import { AnyIterable, StandardJSONSchema, MongoDBJSONSchema, ExpandedJSONSchema } from './types';
2225

@@ -28,7 +31,11 @@ async function analyzeDocuments(
2831
options?: SchemaParseOptions
2932
): Promise<SchemaAccessor> {
3033
const internalSchema = (await getCompletedSchemaAnalyzer(source, options)).getResult();
31-
return new InternalSchemaBasedAccessor(internalSchema);
34+
return new InternalSchemaBasedAccessor(internalSchema, {
35+
internalToStandard: convertInternalToStandard,
36+
internalToMongoDB: convertInternalToMongodb,
37+
internalToExpanded: convertInternalToExpanded
38+
});
3239
}
3340

3441
/**

src/schema-accessor.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { Schema as InternalSchema } from './schema-analyzer';
2-
import { convertors } from './schema-convertors';
3-
import { ExpandedJSONSchema, MongoDBJSONSchema, StandardJSONSchema } from './types';
2+
import { ExpandedJSONSchema, MongoDBJSONSchema, SchemaConverterFn, StandardJSONSchema } from './types';
43

54
type Options = {
65
signal?: AbortSignal;
@@ -19,14 +18,22 @@ export interface SchemaAccessor {
1918
* the others are converted lazily and memoized.
2019
* Conversion can be aborted.
2120
*/
21+
22+
export type InternalConverters = {
23+
internalToStandard: SchemaConverterFn<InternalSchema, StandardJSONSchema>,
24+
internalToExpanded: SchemaConverterFn<InternalSchema, ExpandedJSONSchema>,
25+
internalToMongoDB: SchemaConverterFn<InternalSchema, MongoDBJSONSchema>,
26+
}
2227
export class InternalSchemaBasedAccessor implements SchemaAccessor {
2328
private internalSchema: InternalSchema;
2429
private standardJSONSchema?: StandardJSONSchema;
2530
private mongodbJSONSchema?: MongoDBJSONSchema;
26-
private ExpandedJSONSchema?: ExpandedJSONSchema;
31+
private expandedJSONSchema?: ExpandedJSONSchema;
32+
private converters: InternalConverters;
2733

28-
constructor(internalSchema: InternalSchema) {
34+
constructor(internalSchema: InternalSchema, converters: InternalConverters) {
2935
this.internalSchema = internalSchema;
36+
this.converters = converters;
3037
}
3138

3239
async getInternalSchema(): Promise<InternalSchema> {
@@ -38,20 +45,20 @@ export class InternalSchemaBasedAccessor implements SchemaAccessor {
3845
* https://json-schema.org/draft/2020-12/schema
3946
*/
4047
async getStandardJsonSchema(options: Options = {}): Promise<StandardJSONSchema> {
41-
return this.standardJSONSchema ??= await convertors.internalSchemaToStandard(this.internalSchema, options);
48+
return this.standardJSONSchema ??= await this.converters.internalToStandard(this.internalSchema, options);
4249
}
4350

4451
/**
4552
* Get MongoDB's $jsonSchema
4653
*/
4754
async getMongoDBJsonSchema(options: Options = {}): Promise<MongoDBJSONSchema> {
48-
return this.mongodbJSONSchema ??= await convertors.internalSchemaToMongoDB(this.internalSchema, options);
55+
return this.mongodbJSONSchema ??= await this.converters.internalToMongoDB(this.internalSchema, options);
4956
}
5057

5158
/**
5259
* Get expanded JSON Schema - with additional properties
5360
*/
5461
async getExpandedJSONSchema(options: Options = {}): Promise<ExpandedJSONSchema> {
55-
return this.ExpandedJSONSchema ??= await convertors.internalSchemaToExpanded(this.internalSchema, options);
62+
return this.expandedJSONSchema ??= await this.converters.internalToExpanded(this.internalSchema, options);
5663
}
5764
}

src/schema-convertors/internalToExpanded.test.ts renamed to src/schema-converters/internalToExpanded.test.ts

Lines changed: 40 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import assert from 'assert';
2-
import internalSchemaToExpanded from './internalToExpanded';
32
import { RELAXED_EJSON_DEFINITIONS } from './internalToStandard';
3+
import { convertInternalToExpanded } from './internalToExpanded';
44

55
describe('internalSchemaToExpanded', async function() {
66
describe('Converts: ', async function() {
@@ -336,12 +336,20 @@ describe('internalSchemaToExpanded', async function() {
336336
}
337337
]
338338
};
339-
const standard = await internalSchemaToExpanded(internal);
340-
assert.deepStrictEqual(standard, {
339+
const expanded = await convertInternalToExpanded(internal);
340+
const expectedDefinitions: Partial<typeof RELAXED_EJSON_DEFINITIONS> = { ...RELAXED_EJSON_DEFINITIONS };
341+
delete expectedDefinitions.BSONSymbol;
342+
delete expectedDefinitions.CodeWScope;
343+
delete expectedDefinitions.DBPointer;
344+
delete expectedDefinitions.DBRef;
345+
delete expectedDefinitions.Date;
346+
delete expectedDefinitions.MinKey;
347+
delete expectedDefinitions.Undefined;
348+
assert.deepStrictEqual(expanded, {
341349
type: 'object',
342350
'x-bsonType': 'object',
343351
required: [],
344-
$defs: RELAXED_EJSON_DEFINITIONS,
352+
$defs: expectedDefinitions,
345353
properties: {
346354
_id: {
347355
$ref: '#/$defs/ObjectId',
@@ -593,12 +601,15 @@ describe('internalSchemaToExpanded', async function() {
593601
}
594602
]
595603
};
596-
const standard = await internalSchemaToExpanded(internal);
597-
assert.deepStrictEqual(standard, {
604+
const expanded = await convertInternalToExpanded(internal);
605+
const expectedDefinitions = {
606+
Double: RELAXED_EJSON_DEFINITIONS.Double
607+
};
608+
assert.deepStrictEqual(expanded, {
598609
type: 'object',
599610
'x-bsonType': 'object',
600611
required: ['author'],
601-
$defs: RELAXED_EJSON_DEFINITIONS,
612+
$defs: expectedDefinitions,
602613
properties: {
603614
author: {
604615
type: 'object',
@@ -704,12 +715,12 @@ describe('internalSchemaToExpanded', async function() {
704715
}
705716
]
706717
};
707-
const standard = await internalSchemaToExpanded(internal);
708-
assert.deepStrictEqual(standard, {
718+
const expanded = await convertInternalToExpanded(internal);
719+
assert.deepStrictEqual(expanded, {
709720
type: 'object',
710721
'x-bsonType': 'object',
711722
required: [],
712-
$defs: RELAXED_EJSON_DEFINITIONS,
723+
$defs: {},
713724
properties: {
714725
genres: {
715726
type: 'array',
@@ -867,12 +878,12 @@ describe('internalSchemaToExpanded', async function() {
867878
}
868879
]
869880
};
870-
const standard = await internalSchemaToExpanded(internal);
871-
assert.deepStrictEqual(standard, {
881+
const expanded = await convertInternalToExpanded(internal);
882+
assert.deepStrictEqual(expanded, {
872883
type: 'object',
873884
'x-bsonType': 'object',
874885
required: [],
875-
$defs: RELAXED_EJSON_DEFINITIONS,
886+
$defs: {},
876887
properties: {
877888
genres: {
878889
type: 'array',
@@ -1002,12 +1013,12 @@ describe('internalSchemaToExpanded', async function() {
10021013
}
10031014
]
10041015
};
1005-
const standard = await internalSchemaToExpanded(internal);
1006-
assert.deepStrictEqual(standard, {
1016+
const expanded = await convertInternalToExpanded(internal);
1017+
assert.deepStrictEqual(expanded, {
10071018
type: 'object',
10081019
'x-bsonType': 'object',
10091020
required: ['arrayMixedType'],
1010-
$defs: RELAXED_EJSON_DEFINITIONS,
1021+
$defs: {},
10111022
properties: {
10121023
arrayMixedType: {
10131024
type: 'array',
@@ -1111,12 +1122,12 @@ describe('internalSchemaToExpanded', async function() {
11111122
}
11121123
]
11131124
};
1114-
const standard = await internalSchemaToExpanded(internal);
1115-
assert.deepStrictEqual(standard, {
1125+
const expanded = await convertInternalToExpanded(internal);
1126+
assert.deepStrictEqual(expanded, {
11161127
type: 'object',
11171128
'x-bsonType': 'object',
11181129
required: [],
1119-
$defs: RELAXED_EJSON_DEFINITIONS,
1130+
$defs: {},
11201131
properties: {
11211132
mixedType: {
11221133
'x-metadata': {
@@ -1252,12 +1263,12 @@ describe('internalSchemaToExpanded', async function() {
12521263
}
12531264
]
12541265
};
1255-
const standard = await internalSchemaToExpanded(internal);
1256-
assert.deepStrictEqual(standard, {
1266+
const expanded = await convertInternalToExpanded(internal);
1267+
assert.deepStrictEqual(expanded, {
12571268
type: 'object',
12581269
'x-bsonType': 'object',
12591270
required: [],
1260-
$defs: RELAXED_EJSON_DEFINITIONS,
1271+
$defs: {},
12611272
properties: {
12621273
mixedComplexType: {
12631274
'x-metadata': {
@@ -1360,12 +1371,15 @@ describe('internalSchemaToExpanded', async function() {
13601371
}
13611372
]
13621373
};
1363-
const standard = await internalSchemaToExpanded(internal);
1364-
assert.deepStrictEqual(standard, {
1374+
const expanded = await convertInternalToExpanded(internal);
1375+
const expectedDefinitions = {
1376+
ObjectId: RELAXED_EJSON_DEFINITIONS.ObjectId
1377+
};
1378+
assert.deepStrictEqual(expanded, {
13651379
type: 'object',
13661380
'x-bsonType': 'object',
13671381
required: ['mixedType'],
1368-
$defs: RELAXED_EJSON_DEFINITIONS,
1382+
$defs: expectedDefinitions,
13691383
properties: {
13701384
mixedType: {
13711385
'x-metadata': {
@@ -1507,7 +1521,7 @@ describe('internalSchemaToExpanded', async function() {
15071521
]
15081522
};
15091523
const abortController = new AbortController();
1510-
const promise = internalSchemaToExpanded(internal, { signal: abortController.signal });
1524+
const promise = convertInternalToExpanded(internal, { signal: abortController.signal });
15111525
abortController.abort(new Error('Too long, didn\'t wait.'));
15121526
await assert.rejects(promise, {
15131527
name: 'Error',
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { ArraySchemaType, DocumentSchemaType, Schema as InternalSchema, SchemaType, SchemaField } from '../schema-analyzer';
2+
import { type ExpandedJSONSchema } from '../types';
3+
import { InternalTypeToStandardTypeMap, RELAXED_EJSON_DEFINITIONS } from './internalToStandard';
4+
import { InternalTypeToBsonTypeMap } from './internalToMongoDB';
5+
import { allowAbort } from './util';
6+
7+
const createConvertInternalToExpanded = function() {
8+
const usedDefinitions = new Set<string>();
9+
10+
function getUsedDefinitions() {
11+
const filteredDefinitions = Object.fromEntries(
12+
Object.entries(RELAXED_EJSON_DEFINITIONS).filter(([key]) => usedDefinitions.has(key))
13+
);
14+
return Object.freeze(filteredDefinitions);
15+
}
16+
17+
function markUsedDefinition(ref: string) {
18+
usedDefinitions.add(ref.split('/')[2]);
19+
}
20+
21+
function getStandardType(internalType: string) {
22+
const type = InternalTypeToStandardTypeMap[internalType];
23+
if (!type) throw new Error(`Encountered unknown type: ${internalType}`);
24+
return { ...type };
25+
}
26+
27+
function getBsonType(internalType: string) {
28+
const type = InternalTypeToBsonTypeMap[internalType];
29+
if (!type) throw new Error(`Encountered unknown type: ${internalType}`);
30+
return type;
31+
}
32+
33+
async function parseType(type: SchemaType, signal?: AbortSignal): Promise<ExpandedJSONSchema> {
34+
await allowAbort(signal);
35+
const schema: ExpandedJSONSchema = {
36+
...getStandardType(type.bsonType),
37+
'x-bsonType': getBsonType(type.bsonType),
38+
'x-metadata': getMetadata(type)
39+
};
40+
if ('values' in type && type.values) {
41+
schema['x-sampleValues'] = type.values;
42+
}
43+
if (schema.$ref) markUsedDefinition(schema.$ref);
44+
switch (type.bsonType) {
45+
case 'Array':
46+
schema.items = await parseTypes((type as ArraySchemaType).types, signal);
47+
break;
48+
case 'Document':
49+
Object.assign(schema, await parseFields((type as DocumentSchemaType).fields, signal));
50+
break;
51+
}
52+
return schema;
53+
}
54+
55+
function getMetadata<TType extends SchemaField | SchemaType>({
56+
hasDuplicates,
57+
probability,
58+
count
59+
}: TType) {
60+
return {
61+
...(typeof hasDuplicates === 'boolean' ? { hasDuplicates } : {}),
62+
probability,
63+
count
64+
};
65+
}
66+
67+
async function parseTypes(types: SchemaType[], signal?: AbortSignal): Promise<ExpandedJSONSchema> {
68+
await allowAbort(signal);
69+
const definedTypes = types.filter(type => type.bsonType.toLowerCase() !== 'undefined');
70+
const isSingleType = definedTypes.length === 1;
71+
if (isSingleType) {
72+
return parseType(definedTypes[0], signal);
73+
}
74+
const parsedTypes = await Promise.all(definedTypes.map(type => parseType(type, signal)));
75+
return {
76+
anyOf: parsedTypes
77+
};
78+
}
79+
80+
async function parseFields(
81+
fields: DocumentSchemaType['fields'],
82+
signal?: AbortSignal
83+
): Promise<{ required: ExpandedJSONSchema['required']; properties: ExpandedJSONSchema['properties'] }> {
84+
const required = [];
85+
const properties: ExpandedJSONSchema['properties'] = {};
86+
for (const field of fields) {
87+
if (field.probability === 1) required.push(field.name);
88+
properties[field.name] = {
89+
...await parseTypes(field.types, signal),
90+
'x-metadata': getMetadata(field)
91+
};
92+
}
93+
94+
return { required, properties };
95+
}
96+
97+
return async function convert(
98+
internalSchema: InternalSchema,
99+
options: { signal?: AbortSignal } = {}
100+
): Promise<ExpandedJSONSchema> {
101+
const { required, properties } = await parseFields(internalSchema.fields, options.signal);
102+
const schema: ExpandedJSONSchema = {
103+
type: 'object',
104+
'x-bsonType': 'object',
105+
required,
106+
properties,
107+
$defs: getUsedDefinitions()
108+
};
109+
return schema;
110+
};
111+
};
112+
113+
export function convertInternalToExpanded(
114+
internalSchema: InternalSchema,
115+
options: { signal?: AbortSignal } = {}
116+
): Promise<ExpandedJSONSchema> {
117+
return createConvertInternalToExpanded()(internalSchema, options);
118+
}

0 commit comments

Comments
 (0)