Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ import type {
SimplifiedSchemaField,
SimplifiedSchema
} from './schema-analyzer';
import { convertInternalToExpanded } from './schema-converters/internalToExpanded';
import { convertInternalToMongodb } from './schema-converters/internalToMongoDB';
import { convertInternalToStandard } from './schema-converters/internalToStandard';
import * as schemaStats from './stats';
import { AnyIterable, StandardJSONSchema, MongoDBJSONSchema, ExpandedJSONSchema } from './types';

Expand All @@ -28,7 +31,11 @@ async function analyzeDocuments(
options?: SchemaParseOptions
): Promise<SchemaAccessor> {
const internalSchema = (await getCompletedSchemaAnalyzer(source, options)).getResult();
return new InternalSchemaBasedAccessor(internalSchema);
return new InternalSchemaBasedAccessor(internalSchema, {
internalToStandard: convertInternalToStandard,
internalToMongoDB: convertInternalToMongodb,
internalToExpanded: convertInternalToExpanded
});
}

/**
Expand Down
21 changes: 14 additions & 7 deletions src/schema-accessor.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Schema as InternalSchema } from './schema-analyzer';
import { convertors } from './schema-convertors';
import { ExpandedJSONSchema, MongoDBJSONSchema, StandardJSONSchema } from './types';
import { ExpandedJSONSchema, MongoDBJSONSchema, SchemaConverterFn, StandardJSONSchema } from './types';

export interface SchemaAccessor {
getStandardJsonSchema: () => Promise<StandardJSONSchema>;
Expand All @@ -19,14 +18,22 @@ type Options = {
* the others are converted lazily and memoized.
* Conversion can be aborted.
*/

export type InternalConverters = {
internalToStandard: SchemaConverterFn<InternalSchema, StandardJSONSchema>,
internalToExpanded: SchemaConverterFn<InternalSchema, ExpandedJSONSchema>,
internalToMongoDB: SchemaConverterFn<InternalSchema, MongoDBJSONSchema>,
}
export class InternalSchemaBasedAccessor implements SchemaAccessor {
private internalSchema: InternalSchema;
private standardJSONSchema?: StandardJSONSchema;
private mongodbJSONSchema?: MongoDBJSONSchema;
private ExpandedJSONSchema?: ExpandedJSONSchema;
private expandedJSONSchema?: ExpandedJSONSchema;
private converters: InternalConverters;

constructor(internalSchema: InternalSchema) {
constructor(internalSchema: InternalSchema, converters: InternalConverters) {
this.internalSchema = internalSchema;
this.converters = converters;
}

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

/**
* Get MongoDB's $jsonSchema
*/
async getMongoDBJsonSchema(options: Options = {}): Promise<MongoDBJSONSchema> {
return this.mongodbJSONSchema ??= await convertors.internalSchemaToMongoDB(this.internalSchema, options);
return this.mongodbJSONSchema ??= await this.converters.internalToMongoDB(this.internalSchema, options);
}

/**
* Get expanded JSON Schema - with additional properties
*/
async getExpandedJSONSchema(options: Options = {}): Promise<ExpandedJSONSchema> {
return this.ExpandedJSONSchema ??= await convertors.internalSchemaToExpanded(this.internalSchema, options);
return this.expandedJSONSchema ??= await this.converters.internalToExpanded(this.internalSchema, options);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import assert from 'assert';
import internalSchemaToExpanded from './internalToExpanded';
import { RELAXED_EJSON_DEFINITIONS } from './internalToStandard';
import { convertInternalToExpanded } from './internalToExpanded';

describe('internalSchemaToExpanded', async function() {
describe('Converts: ', async function() {
Expand Down Expand Up @@ -336,12 +336,20 @@ describe('internalSchemaToExpanded', async function() {
}
]
};
const standard = await internalSchemaToExpanded(internal);
assert.deepStrictEqual(standard, {
const expanded = await convertInternalToExpanded(internal);
const expectedDefinitions: any = RELAXED_EJSON_DEFINITIONS;
delete expectedDefinitions.BSONSymbol;
delete expectedDefinitions.CodeWScope;
delete expectedDefinitions.DBPointer;
delete expectedDefinitions.DBRef;
delete expectedDefinitions.Date;
delete expectedDefinitions.MinKey;
delete expectedDefinitions.Undefined;
assert.deepStrictEqual(expanded, {
type: 'object',
'x-bsonType': 'object',
required: [],
$defs: RELAXED_EJSON_DEFINITIONS,
$defs: expectedDefinitions,
properties: {
_id: {
$ref: '#/$defs/ObjectId',
Expand Down Expand Up @@ -593,12 +601,15 @@ describe('internalSchemaToExpanded', async function() {
}
]
};
const standard = await internalSchemaToExpanded(internal);
assert.deepStrictEqual(standard, {
const expanded = await convertInternalToExpanded(internal);
const expectedDefinitions = {
Double: RELAXED_EJSON_DEFINITIONS.Double
};
assert.deepStrictEqual(expanded, {
type: 'object',
'x-bsonType': 'object',
required: ['author'],
$defs: RELAXED_EJSON_DEFINITIONS,
$defs: expectedDefinitions,
properties: {
author: {
type: 'object',
Expand Down Expand Up @@ -704,12 +715,12 @@ describe('internalSchemaToExpanded', async function() {
}
]
};
const standard = await internalSchemaToExpanded(internal);
assert.deepStrictEqual(standard, {
const expanded = await convertInternalToExpanded(internal);
assert.deepStrictEqual(expanded, {
type: 'object',
'x-bsonType': 'object',
required: [],
$defs: RELAXED_EJSON_DEFINITIONS,
$defs: {},
properties: {
genres: {
type: 'array',
Expand Down Expand Up @@ -867,12 +878,12 @@ describe('internalSchemaToExpanded', async function() {
}
]
};
const standard = await internalSchemaToExpanded(internal);
assert.deepStrictEqual(standard, {
const expanded = await convertInternalToExpanded(internal);
assert.deepStrictEqual(expanded, {
type: 'object',
'x-bsonType': 'object',
required: [],
$defs: RELAXED_EJSON_DEFINITIONS,
$defs: {},
properties: {
genres: {
type: 'array',
Expand Down Expand Up @@ -1002,12 +1013,12 @@ describe('internalSchemaToExpanded', async function() {
}
]
};
const standard = await internalSchemaToExpanded(internal);
assert.deepStrictEqual(standard, {
const expanded = await convertInternalToExpanded(internal);
assert.deepStrictEqual(expanded, {
type: 'object',
'x-bsonType': 'object',
required: ['arrayMixedType'],
$defs: RELAXED_EJSON_DEFINITIONS,
$defs: {},
properties: {
arrayMixedType: {
type: 'array',
Expand Down Expand Up @@ -1111,12 +1122,12 @@ describe('internalSchemaToExpanded', async function() {
}
]
};
const standard = await internalSchemaToExpanded(internal);
assert.deepStrictEqual(standard, {
const expanded = await convertInternalToExpanded(internal);
assert.deepStrictEqual(expanded, {
type: 'object',
'x-bsonType': 'object',
required: [],
$defs: RELAXED_EJSON_DEFINITIONS,
$defs: {},
properties: {
mixedType: {
'x-metadata': {
Expand Down Expand Up @@ -1252,12 +1263,12 @@ describe('internalSchemaToExpanded', async function() {
}
]
};
const standard = await internalSchemaToExpanded(internal);
assert.deepStrictEqual(standard, {
const expanded = await convertInternalToExpanded(internal);
assert.deepStrictEqual(expanded, {
type: 'object',
'x-bsonType': 'object',
required: [],
$defs: RELAXED_EJSON_DEFINITIONS,
$defs: {},
properties: {
mixedComplexType: {
'x-metadata': {
Expand Down Expand Up @@ -1360,12 +1371,15 @@ describe('internalSchemaToExpanded', async function() {
}
]
};
const standard = await internalSchemaToExpanded(internal);
assert.deepStrictEqual(standard, {
const expanded = await convertInternalToExpanded(internal);
const expectedDefinitions = {
ObjectId: RELAXED_EJSON_DEFINITIONS.ObjectId
};
assert.deepStrictEqual(expanded, {
type: 'object',
'x-bsonType': 'object',
required: ['mixedType'],
$defs: RELAXED_EJSON_DEFINITIONS,
$defs: expectedDefinitions,
properties: {
mixedType: {
'x-metadata': {
Expand Down Expand Up @@ -1507,7 +1521,7 @@ describe('internalSchemaToExpanded', async function() {
]
};
const abortController = new AbortController();
const promise = internalSchemaToExpanded(internal, { signal: abortController.signal });
const promise = convertInternalToExpanded(internal, { signal: abortController.signal });
abortController.abort(new Error('Too long, didn\'t wait.'));
await assert.rejects(promise, {
name: 'Error',
Expand Down
118 changes: 118 additions & 0 deletions src/schema-converters/internalToExpanded.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { ArraySchemaType, DocumentSchemaType, Schema as InternalSchema, SchemaType, SchemaField } from '../schema-analyzer';
import { type ExpandedJSONSchema } from '../types';
import { InternalTypeToStandardTypeMap, RELAXED_EJSON_DEFINITIONS } from './internalToStandard';
import { InternalTypeToBsonTypeMap } from './internalToMongoDB';
import { allowAbort } from './util';

const createConvertInternalToExpanded = function() {
const usedDefinitions = new Set<string>();

function getUsedDefinitions() {
const filteredDefinitions = Object.fromEntries(
Object.entries(RELAXED_EJSON_DEFINITIONS).filter(([key]) => usedDefinitions.has(key))
);
return Object.freeze(filteredDefinitions);
}

function markUsedDefinition(ref: string) {
usedDefinitions.add(ref.split('/')[2]);
}

function getStandardType(internalType: string) {
const type = InternalTypeToStandardTypeMap[internalType];
if (!type) throw new Error(`Encountered unknown type: ${internalType}`);
return { ...type };
}

function getBsonType(internalType: string) {
const type = InternalTypeToBsonTypeMap[internalType];
if (!type) throw new Error(`Encountered unknown type: ${internalType}`);
return type;
}

async function parseType(type: SchemaType, signal?: AbortSignal): Promise<ExpandedJSONSchema> {
await allowAbort(signal);
const schema: ExpandedJSONSchema = {
...getStandardType(type.bsonType),
'x-bsonType': getBsonType(type.bsonType),
'x-metadata': getMetadata(type)
};
if ('values' in type && type.values) {
schema['x-sampleValues'] = type.values;
}
if (schema.$ref) markUsedDefinition(schema.$ref);
switch (type.bsonType) {
case 'Array':
schema.items = await parseTypes((type as ArraySchemaType).types, signal);
break;
case 'Document':
Object.assign(schema, await parseFields((type as DocumentSchemaType).fields, signal));
break;
}
return schema;
}

function getMetadata<TType extends SchemaField | SchemaType>({
hasDuplicates,
probability,
count
}: TType) {
return {
...(typeof hasDuplicates === 'boolean' ? { hasDuplicates } : {}),
probability,
count
};
}

async function parseTypes(types: SchemaType[], signal?: AbortSignal): Promise<ExpandedJSONSchema> {
await allowAbort(signal);
const definedTypes = types.filter(type => type.bsonType.toLowerCase() !== 'undefined');
const isSingleType = definedTypes.length === 1;
if (isSingleType) {
return parseType(definedTypes[0], signal);
}
const parsedTypes = await Promise.all(definedTypes.map(type => parseType(type, signal)));
return {
anyOf: parsedTypes
};
}

async function parseFields(
fields: DocumentSchemaType['fields'],
signal?: AbortSignal
): Promise<{ required: ExpandedJSONSchema['required']; properties: ExpandedJSONSchema['properties'] }> {
const required = [];
const properties: ExpandedJSONSchema['properties'] = {};
for (const field of fields) {
if (field.probability === 1) required.push(field.name);
properties[field.name] = {
...await parseTypes(field.types, signal),
'x-metadata': getMetadata(field)
};
}

return { required, properties };
}

return async function convert(
internalSchema: InternalSchema,
options: { signal?: AbortSignal } = {}
): Promise<ExpandedJSONSchema> {
const { required, properties } = await parseFields(internalSchema.fields, options.signal);
const schema: ExpandedJSONSchema = {
type: 'object',
'x-bsonType': 'object',
required,
properties,
$defs: getUsedDefinitions()
};
return schema;
};
};

export function convertInternalToExpanded(
internalSchema: InternalSchema,
options: { signal?: AbortSignal } = {}
): Promise<ExpandedJSONSchema> {
return createConvertInternalToExpanded()(internalSchema, options);
}
Loading
Loading