diff --git a/src/schema-convertors/internalToExpanded.test.ts b/src/schema-convertors/internalToExpanded.test.ts new file mode 100644 index 0000000..0999296 --- /dev/null +++ b/src/schema-convertors/internalToExpanded.test.ts @@ -0,0 +1,1518 @@ +import assert from 'assert'; +import internalSchemaToExpanded from './internalToExpanded'; +import { RELAXED_EJSON_DEFINITIONS } from './internalToStandard'; + +describe('internalSchemaToExpanded', async function() { + describe('Converts: ', async function() { + it('various types', async function() { + const internal = { + count: 1, + fields: [ + // types with ref + { + name: '_id', + path: [ + '_id' + ], + count: 1, + type: 'ObjectId', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'ObjectId', + path: [ + '_id' + ], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: [ + '642d766b7300158b1f22e972' + ], + bsonType: 'ObjectId' + } + ] + }, + { + name: 'binaries', + path: [ + 'binaries' + ], + count: 1, + type: 'Document', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'Document', + path: [ + 'binaries' + ], + count: 1, + probability: 0.8, + bsonType: 'Document', + fields: [ + { + name: 'binaryOld', + path: [ + 'binaries', + 'binaryOld' + ], + count: 1, + type: 'Binary', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'Binary', + path: [ + 'binaries', + 'binaryOld' + ], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: [ + '//8=' + ], + bsonType: 'Binary' + } + ] + } + ] + } + ] + }, + { + name: 'binData', + path: [ + 'binData' + ], + count: 1, + type: 'Binary', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'Binary', + path: [ + 'binData' + ], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: [ + 'AQID' + ], + bsonType: 'Binary' + } + ] + }, + // type with different standard and bsonType + { + name: 'boolean', + path: [ + 'boolean' + ], + count: 1, + type: 'Boolean', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'Boolean', + path: [ + 'boolean' + ], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: [ + true + ], + bsonType: 'Boolean' + } + ] + }, + { + name: 'decimal', + path: [ + 'decimal' + ], + count: 1, + type: 'Decimal128', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'Decimal128', + path: [ + 'decimal' + ], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: [ + { + $numberDecimal: '5.477284286264328586719275128128001E-4088' + } + ], + bsonType: 'Decimal128' + } + ] + }, + { + name: 'double', + path: [ + 'double' + ], + count: 1, + type: 'Double', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'Double', + path: [ + 'double' + ], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: [ + 1.2 + ], + bsonType: 'Double' + } + ] + }, + { + name: 'javascript', + path: [ + 'javascript' + ], + count: 1, + type: 'Code', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'Code', + path: [ + 'javascript' + ], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: [ + { + code: 'function() {}' + } + ], + bsonType: 'Code' + } + ] + }, + { + name: 'long', + path: [ + 'long' + ], + count: 1, + type: 'Long', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'Long', + path: [ + 'long' + ], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: [ + { + low: -1395630315, + high: 28744523, + unsigned: false + } + ], + bsonType: 'Long' + } + ] + }, + { + name: 'maxKey', + path: [ + 'maxKey' + ], + count: 1, + type: 'MaxKey', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'MaxKey', + path: [ + 'maxKey' + ], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: [ + {} + ], + bsonType: 'MaxKey' + } + ] + }, + { + name: 'regex', + path: [ + 'regex' + ], + count: 1, + type: 'BSONRegExp', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'BSONRegExp', + path: [ + 'regex' + ], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: [ + { + pattern: 'pattern', + options: 'i' + } + ], + bsonType: 'BSONRegExp' + } + ] + }, + { + name: 'timestamp', + path: [ + 'timestamp' + ], + count: 1, + type: 'Timestamp', + probability: 0.8, + hasDuplicates: false, + types: [ + { + name: 'Timestamp', + path: [ + 'timestamp' + ], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: [ + { + $timestamp: '7218556297505931265' + } + ], + bsonType: 'Timestamp' + } + ] + } + ] + }; + const standard = await internalSchemaToExpanded(internal); + assert.deepStrictEqual(standard, { + type: 'object', + 'x-bsonType': 'object', + required: [], + $defs: RELAXED_EJSON_DEFINITIONS, + properties: { + _id: { + $ref: '#/$defs/ObjectId', + 'x-bsonType': 'objectId', + 'x-metadata': { + count: 1, + hasDuplicates: false, + probability: 0.8 + }, + 'x-sampleValues': [ + '642d766b7300158b1f22e972' + ] + }, + binData: { + $ref: '#/$defs/Binary', + 'x-bsonType': 'binData', + 'x-metadata': { + count: 1, + hasDuplicates: false, + probability: 0.8 + }, + 'x-sampleValues': [ + 'AQID' + ] + }, + binaries: { + type: 'object', + 'x-bsonType': 'object', + 'x-metadata': { + count: 1, + hasDuplicates: false, + probability: 0.8 + }, + properties: { + binaryOld: { + $ref: '#/$defs/Binary', + 'x-bsonType': 'binData', + 'x-metadata': { + count: 1, + hasDuplicates: false, + probability: 0.8 + }, + 'x-sampleValues': [ + '//8=' + ] + } + }, + required: [] + }, + boolean: { + type: 'boolean', + 'x-bsonType': 'bool', + 'x-metadata': { + count: 1, + hasDuplicates: false, + probability: 0.8 + }, + 'x-sampleValues': [ + true + ] + }, + decimal: { + $ref: '#/$defs/Decimal128', + 'x-bsonType': 'decimal', + 'x-metadata': { + count: 1, + hasDuplicates: false, + probability: 0.8 + }, + 'x-sampleValues': [{ + $numberDecimal: '5.477284286264328586719275128128001E-4088' + }] + }, + double: { + $ref: '#/$defs/Double', + 'x-bsonType': 'double', + 'x-metadata': { + count: 1, + hasDuplicates: false, + probability: 0.8 + }, + 'x-sampleValues': [ + 1.2 + ] + }, + javascript: { + $ref: '#/$defs/Code', + 'x-bsonType': 'javascript', + 'x-metadata': { + count: 1, + hasDuplicates: false, + probability: 0.8 + }, + 'x-sampleValues': [{ + code: 'function() {}' + }] + }, + long: { + type: 'integer', + 'x-bsonType': 'long', + 'x-metadata': { + count: 1, + hasDuplicates: false, + probability: 0.8 + }, + 'x-sampleValues': [{ + low: -1395630315, + high: 28744523, + unsigned: false + }] + }, + maxKey: { + $ref: '#/$defs/MaxKey', + 'x-bsonType': 'maxKey', + 'x-metadata': { + count: 1, + hasDuplicates: false, + probability: 0.8 + }, + 'x-sampleValues': [ + {} + ] + }, + regex: { + $ref: '#/$defs/RegExp', + 'x-bsonType': 'regex', + 'x-metadata': { + count: 1, + hasDuplicates: false, + probability: 0.8 + }, + 'x-sampleValues': [{ + options: 'i', + pattern: 'pattern' + }] + }, + timestamp: { + $ref: '#/$defs/Timestamp', + 'x-bsonType': 'timestamp', + 'x-metadata': { + count: 1, + hasDuplicates: false, + probability: 0.8 + }, + 'x-sampleValues': [{ + $timestamp: '7218556297505931265' + }] + } + } + }); + }); + + it('nested document/object', async function() { + const internal = { + count: 2, + fields: [ + { + name: 'author', + path: [ + 'author' + ], + count: 1, + type: [ + 'Document', + 'Undefined' + ], + probability: 1, + hasDuplicates: false, + types: [ + { + name: 'Document', + path: [ + 'author' + ], + count: 1, + probability: 0.5, + bsonType: 'Document', + fields: [ + { + name: 'name', + path: [ + 'author', + 'name' + ], + count: 1, + type: 'String', + probability: 1, + hasDuplicates: false, + types: [ + { + name: 'String', + path: [ + 'author', + 'name' + ], + count: 1, + probability: 1, + unique: 1, + hasDuplicates: false, + values: [ + 'Peter Sonder' + ], + bsonType: 'String' + } + ] + }, + { + name: 'rating', + path: [ + 'author', + 'rating' + ], + count: 1, + type: 'Double', + probability: 1, + hasDuplicates: false, + types: [ + { + name: 'Double', + path: [ + 'author', + 'rating' + ], + count: 1, + probability: 1, + unique: 1, + hasDuplicates: false, + values: [ + 1.3 + ], + bsonType: 'Double' + } + ] + } + ] + }, + { + name: 'Undefined', + bsonType: 'Undefined', + unique: 1, + hasDuplicates: false, + path: [ + 'author' + ], + count: 1, + probability: 0.5 + } + ] + } + ] + }; + const standard = await internalSchemaToExpanded(internal); + assert.deepStrictEqual(standard, { + type: 'object', + 'x-bsonType': 'object', + required: ['author'], + $defs: RELAXED_EJSON_DEFINITIONS, + properties: { + author: { + type: 'object', + 'x-bsonType': 'object', + 'x-metadata': { + count: 1, + hasDuplicates: false, + probability: 1 + }, + required: ['name', 'rating'], + properties: { + name: { + type: 'string', + 'x-bsonType': 'string', + 'x-metadata': { + count: 1, + hasDuplicates: false, + probability: 1 + }, + 'x-sampleValues': [ + 'Peter Sonder' + ] + }, + rating: { + $ref: '#/$defs/Double', + 'x-bsonType': 'double', + 'x-metadata': { + count: 1, + hasDuplicates: false, + probability: 1 + }, + 'x-sampleValues': [ + 1.3 + ] + } + } + } + } + }); + }); + + describe('arrays', async function() { + it('array - single type', async function() { + const internal = { + count: 2, + fields: [ + { + name: 'genres', + path: [ + 'genres' + ], + count: 1, + type: [ + 'array', + 'Undefined' + ], + probability: 0.5, + hasDuplicates: false, + types: [ + { + name: 'array', + path: [ + 'genres' + ], + count: 1, + probability: 0.5, + bsonType: 'Array', + types: [ + { + name: 'String', + path: [ + 'genres' + ], + count: 2, + probability: 1, + unique: 2, + hasDuplicates: false, + values: [ + 'crimi', + 'comedy' + ], + bsonType: 'String' + } + ], + totalCount: 2, + lengths: [ + 2 + ], + averageLength: 2 + }, + { + name: 'Undefined', + bsonType: 'Undefined', + unique: 1, + hasDuplicates: false, + path: [ + 'genres' + ], + count: 1, + probability: 0.5 + } + ] + } + ] + }; + const standard = await internalSchemaToExpanded(internal); + assert.deepStrictEqual(standard, { + type: 'object', + 'x-bsonType': 'object', + required: [], + $defs: RELAXED_EJSON_DEFINITIONS, + properties: { + genres: { + type: 'array', + 'x-bsonType': 'array', + 'x-metadata': { + probability: 0.5, + hasDuplicates: false, + count: 1 + }, + items: { + type: 'string', + 'x-bsonType': 'string', + 'x-metadata': { + count: 2, + probability: 1, + hasDuplicates: false + }, + 'x-sampleValues': [ + 'crimi', + 'comedy' + ] + } + } + } + }); + }); + + it('array - mixed type', async function() { + const internal = { + count: 2, + fields: [ + { + name: 'genres', + path: [ + 'genres' + ], + count: 1, + type: [ + 'Array', + 'Undefined' + ], + probability: 0.5, + hasDuplicates: false, + types: [ + { + name: 'Array', + path: [ + 'genres' + ], + count: 1, + probability: 0.5, + bsonType: 'Array', + types: [ + { + name: 'String', + path: [ + 'genres' + ], + count: 2, + probability: 0.6666666666666666, + unique: 2, + hasDuplicates: false, + values: [ + 'crimi', + 'comedy' + ], + bsonType: 'String' + }, + { + name: 'Document', + path: [ + 'genres' + ], + count: 1, + probability: 0.3333333333333333, + bsonType: 'Document', + fields: [ + { + name: 'long', + path: [ + 'genres', + 'long' + ], + count: 1, + type: 'String', + probability: 1, + hasDuplicates: false, + types: [ + { + name: 'String', + path: [ + 'genres', + 'long' + ], + count: 1, + probability: 1, + unique: 1, + hasDuplicates: false, + values: [ + 'science fiction' + ], + bsonType: 'String' + } + ] + }, + { + name: 'short', + path: [ + 'genres', + 'short' + ], + count: 1, + type: 'String', + probability: 1, + hasDuplicates: false, + types: [ + { + name: 'String', + path: [ + 'genres', + 'short' + ], + count: 1, + probability: 1, + unique: 1, + hasDuplicates: false, + values: [ + 'scifi' + ], + bsonType: 'String' + } + ] + } + ] + } + ], + totalCount: 3, + lengths: [ + 3 + ], + averageLength: 3 + }, + { + name: 'Undefined', + bsonType: 'Undefined', + unique: 1, + hasDuplicates: false, + path: [ + 'genres' + ], + count: 1, + probability: 0.5 + } + ] + } + ] + }; + const standard = await internalSchemaToExpanded(internal); + assert.deepStrictEqual(standard, { + type: 'object', + 'x-bsonType': 'object', + required: [], + $defs: RELAXED_EJSON_DEFINITIONS, + properties: { + genres: { + type: 'array', + 'x-bsonType': 'array', + 'x-metadata': { + probability: 0.5, + hasDuplicates: false, + count: 1 + }, + items: { + anyOf: [ + { + type: 'string', + 'x-bsonType': 'string', + 'x-metadata': { + count: 2, + probability: 0.6666666666666666, + hasDuplicates: false + }, + 'x-sampleValues': [ + 'crimi', + 'comedy' + ] + }, + { + type: 'object', + 'x-bsonType': 'object', + 'x-metadata': { + count: 1, + probability: 0.3333333333333333 + }, + required: ['long', 'short'], + properties: { + long: { + type: 'string', + 'x-bsonType': 'string', + 'x-metadata': { + count: 1, + probability: 1, + hasDuplicates: false + }, + 'x-sampleValues': [ + 'science fiction' + ] + }, + short: { + type: 'string', + 'x-bsonType': 'string', + 'x-metadata': { + count: 1, + probability: 1, + hasDuplicates: false + }, + 'x-sampleValues': [ + 'scifi' + ] + } + } + } + ] + } + } + } + }); + }); + + it('array - simple mixed type', async function() { + const internal = { + count: 2, + fields: [ + { + name: 'arrayMixedType', + path: [ + 'arrayMixedType' + ], + count: 1, + type: 'Array', + probability: 1, + hasDuplicates: false, + types: [ + { + name: 'Array', + path: [ + 'arrayMixedType' + ], + count: 1, + probability: 1, + bsonType: 'Array', + types: [ + { + name: 'int32', + path: [ + 'arrayMixedType' + ], + count: 2, + probability: 0.6666666666666666, + unique: 2, + hasDuplicates: false, + values: [ + 1, + 3 + ], + bsonType: 'Int32' + }, + { + name: 'String', + path: [ + 'arrayMixedType' + ], + count: 1, + probability: 0.3333333333333333, + unique: 1, + hasDuplicates: false, + values: [ + '2' + ], + bsonType: 'String' + } + ], + totalCount: 3, + lengths: [ + 3 + ], + averageLength: 3 + } + ] + } + ] + }; + const standard = await internalSchemaToExpanded(internal); + assert.deepStrictEqual(standard, { + type: 'object', + 'x-bsonType': 'object', + required: ['arrayMixedType'], + $defs: RELAXED_EJSON_DEFINITIONS, + properties: { + arrayMixedType: { + type: 'array', + 'x-bsonType': 'array', + 'x-metadata': { + count: 1, + probability: 1, + hasDuplicates: false + }, + items: { + anyOf: [{ + type: 'integer', + 'x-bsonType': 'int', + 'x-metadata': { + count: 2, + hasDuplicates: false, + probability: 0.6666666666666666 + }, + 'x-sampleValues': [ + 1, + 3 + ] + }, { + type: 'string', + 'x-bsonType': 'string', + 'x-metadata': { + count: 1, + hasDuplicates: false, + probability: 0.3333333333333333 + }, + 'x-sampleValues': [ + '2' + ] + }] + } + } + } + }); + }); + }); + + describe('mixed types', async function() { + it('simple mixed type', async function() { + const internal = { + count: 2, + fields: [ + { + name: 'mixedType', + path: [ + 'mixedType' + ], + count: 2, + type: [ + 'Int32', + 'String', + 'Undefined' + ], + probability: 0.6666666666666666, + hasDuplicates: false, + types: [ + { + name: 'Int32', + path: [ + 'mixedType' + ], + count: 1, + probability: 0.3333333333333333, + unique: 1, + hasDuplicates: false, + values: [ + 1 + ], + bsonType: 'Int32' + }, + { + name: 'String', + path: [ + 'mixedType' + ], + count: 1, + probability: 0.3333333333333333, + unique: 1, + hasDuplicates: false, + values: [ + 'abc' + ], + bsonType: 'String' + }, + { + name: 'Undefined', + bsonType: 'Undefined', + unique: 1, + hasDuplicates: false, + path: [ + 'mixedType' + ], + count: 1, + probability: 0.3333333333333333 + } + ] + } + ] + }; + const standard = await internalSchemaToExpanded(internal); + assert.deepStrictEqual(standard, { + type: 'object', + 'x-bsonType': 'object', + required: [], + $defs: RELAXED_EJSON_DEFINITIONS, + properties: { + mixedType: { + 'x-metadata': { + probability: 0.6666666666666666, + hasDuplicates: false, + count: 2 + }, + anyOf: [{ + type: 'integer', + 'x-bsonType': 'int', + 'x-metadata': { + probability: 0.3333333333333333, + hasDuplicates: false, + count: 1 + }, + 'x-sampleValues': [1] + }, { + type: 'string', + 'x-bsonType': 'string', + 'x-metadata': { + probability: 0.3333333333333333, + hasDuplicates: false, + count: 1 + }, + 'x-sampleValues': ['abc'] + }] + } + } + }); + }); + + it('complex mixed type (with array and object)', async function() { + const internal = { + count: 2, + fields: [ + { + name: 'mixedComplexType', + path: [ + 'mixedComplexType' + ], + count: 2, + type: [ + 'Array', + 'Document', + 'Undefined' + ], + probability: 0.6666666666666666, + hasDuplicates: false, + types: [ + { + name: 'Array', + path: [ + 'mixedComplexType' + ], + count: 1, + probability: 0.3333333333333333, + bsonType: 'Array', + types: [ + { + name: 'Int32', + path: [ + 'mixedComplexType' + ], + count: 3, + probability: 1, + unique: 3, + hasDuplicates: false, + values: [ + 1, + 2, + 3 + ], + bsonType: 'Int32' + } + ], + totalCount: 3, + lengths: [ + 3 + ], + averageLength: 3 + }, + { + name: 'Document', + path: [ + 'mixedComplexType' + ], + count: 1, + probability: 0.3333333333333333, + bsonType: 'Document', + fields: [ + { + name: 'a', + path: [ + 'mixedComplexType', + 'a' + ], + count: 1, + type: 'String', + probability: 1, + hasDuplicates: false, + types: [ + { + name: 'String', + path: [ + 'mixedComplexType', + 'a' + ], + count: 1, + probability: 1, + unique: 1, + hasDuplicates: false, + values: [ + 'bc' + ], + bsonType: 'String' + } + ] + } + ] + }, + { + name: 'Undefined', + bsonType: 'Undefined', + unique: 1, + hasDuplicates: false, + path: [ + 'mixedComplexType' + ], + count: 1, + probability: 0.3333333333333333 + } + ] + } + ] + }; + const standard = await internalSchemaToExpanded(internal); + assert.deepStrictEqual(standard, { + type: 'object', + 'x-bsonType': 'object', + required: [], + $defs: RELAXED_EJSON_DEFINITIONS, + properties: { + mixedComplexType: { + 'x-metadata': { + probability: 0.6666666666666666, + hasDuplicates: false, + count: 2 + }, + anyOf: [ + { + type: 'array', + 'x-bsonType': 'array', + 'x-metadata': { + count: 1, + probability: 0.3333333333333333 + }, + items: { + type: 'integer', + 'x-bsonType': 'int', + 'x-metadata': { + probability: 1, + hasDuplicates: false, + count: 3 + }, + 'x-sampleValues': [1, 2, 3] + } + }, + { + type: 'object', + 'x-bsonType': 'object', + required: ['a'], + 'x-metadata': { + count: 1, + probability: 0.3333333333333333 + }, + properties: { + a: { + type: 'string', + 'x-bsonType': 'string', + 'x-metadata': { + probability: 1, + hasDuplicates: false, + count: 1 + }, + 'x-sampleValues': ['bc'] + } + } + } + ] + } + } + }); + }); + + it('complex mixed type (with $refs)', async function() { + const internal = { + count: 2, + fields: [ + { + name: 'mixedType', + path: [ + 'mixedType' + ], + count: 2, + type: [ + 'String', + 'ObjectId' + ], + probability: 1, + hasDuplicates: false, + types: [ + { + name: 'String', + path: [ + 'mixedType' + ], + count: 1, + probability: 0.3333333333333333, + unique: 1, + hasDuplicates: false, + values: [ + 'abc' + ], + bsonType: 'String' + }, + { + name: 'ObjectId', + path: [ + 'objectId' + ], + count: 1, + probability: 0.8, + unique: 1, + hasDuplicates: false, + values: [ + '642d766c7300158b1f22e975' + ], + bsonType: 'ObjectId' + } + ] + } + ] + }; + const standard = await internalSchemaToExpanded(internal); + assert.deepStrictEqual(standard, { + type: 'object', + 'x-bsonType': 'object', + required: ['mixedType'], + $defs: RELAXED_EJSON_DEFINITIONS, + properties: { + mixedType: { + 'x-metadata': { + count: 2, + hasDuplicates: false, + probability: 1 + }, + anyOf: [{ + type: 'string', + 'x-bsonType': 'string', + 'x-metadata': { + count: 1, + hasDuplicates: false, + probability: 0.3333333333333333 + }, + 'x-sampleValues': [ + 'abc' + ] + }, { + $ref: '#/$defs/ObjectId', + 'x-bsonType': 'objectId', + 'x-metadata': { + count: 1, + hasDuplicates: false, + probability: 0.8 + }, + 'x-sampleValues': [ + '642d766c7300158b1f22e975' + ] + }] + } + } + }); + }); + }); + + it('can be aborted', async function() { + const internal = { + count: 2, + fields: [ + { + name: 'mixedComplexType', + path: [ + 'mixedComplexType' + ], + count: 2, + type: [ + 'Array', + 'Document', + 'Undefined' + ], + probability: 0.6666666666666666, + hasDuplicates: false, + types: [ + { + name: 'Array', + path: [ + 'mixedComplexType' + ], + count: 1, + probability: 0.3333333333333333, + bsonType: 'Array', + types: [ + { + name: 'Int32', + path: [ + 'mixedComplexType' + ], + count: 3, + probability: 1, + unique: 3, + hasDuplicates: false, + values: [ + 1, + 2, + 3 + ], + bsonType: 'Int32' + } + ], + totalCount: 3, + lengths: [ + 3 + ], + averageLength: 3 + }, + { + name: 'Document', + path: [ + 'mixedComplexType' + ], + count: 1, + probability: 0.3333333333333333, + bsonType: 'Document', + fields: [ + { + name: 'a', + path: [ + 'mixedComplexType', + 'a' + ], + count: 1, + type: 'String', + probability: 1, + hasDuplicates: false, + types: [ + { + name: 'String', + path: [ + 'mixedComplexType', + 'a' + ], + count: 1, + probability: 1, + unique: 1, + hasDuplicates: false, + values: [ + 'bc' + ], + bsonType: 'String' + } + ] + } + ] + }, + { + name: 'Undefined', + bsonType: 'Undefined', + unique: 1, + hasDuplicates: false, + path: [ + 'mixedComplexType' + ], + count: 1, + probability: 0.3333333333333333 + } + ] + } + ] + }; + const abortController = new AbortController(); + const promise = internalSchemaToExpanded(internal, { signal: abortController.signal }); + abortController.abort(new Error('Too long, didn\'t wait.')); + await assert.rejects(promise, { + name: 'Error', + message: 'Too long, didn\'t wait.' + }); + }); + }); +}); diff --git a/src/schema-convertors/internalToExpanded.ts b/src/schema-convertors/internalToExpanded.ts index be91d93..be3256e 100644 --- a/src/schema-convertors/internalToExpanded.ts +++ b/src/schema-convertors/internalToExpanded.ts @@ -1,12 +1,97 @@ -import { InternalSchema } from '..'; -import { ExpandedJSONSchema } from '../types'; +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'; -export default function internalSchemaToExpanded( - /* eslint @typescript-eslint/no-unused-vars: 0 */ +const getStandardType = (internalType: string) => { + const type = { ...InternalTypeToStandardTypeMap[internalType] }; + if (!type) throw new Error(`Encountered unknown type: ${internalType}`); + return type; +}; + +const 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 { + 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; + } + switch (type.bsonType) { + case 'Array': + schema.items = await parseTypes((type as ArraySchemaType).types); + break; + case 'Document': + Object.assign(schema, + await parseFields((type as DocumentSchemaType).fields, signal) + ); + break; + } + + return schema; +} + +const getMetadata = ({ + hasDuplicates, + probability, + count +}: TType) => ({ + ...(typeof hasDuplicates === 'boolean' ? { hasDuplicates } : {}), + probability, + count + }); + +async function parseTypes(types: SchemaType[], signal?: AbortSignal): Promise { + 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 }; +} + +export default async function internalSchemaToMongodb( internalSchema: InternalSchema, options: { signal?: AbortSignal -}): Promise { - // TODO: COMPASS-8702 - return Promise.resolve({} as ExpandedJSONSchema); +} = {}): Promise { + const { required, properties } = await parseFields(internalSchema.fields, options.signal); + const schema: ExpandedJSONSchema = { + type: 'object', + 'x-bsonType': 'object', + required, + properties, + $defs: RELAXED_EJSON_DEFINITIONS + }; + return schema; } diff --git a/src/types.ts b/src/types.ts index 5cd51e2..272bf06 100644 --- a/src/types.ts +++ b/src/types.ts @@ -10,13 +10,16 @@ export type MongoDBJSONSchema = Pick; + items?: ExpandedJSONSchema | ExpandedJSONSchema[]; + anyOf?: ExpandedJSONSchema[]; } export type AnyIterable = Iterable | AsyncIterable; diff --git a/test/integration/generateAndValidate.ts b/test/integration/generateAndValidate.ts index de2c3c4..e124041 100644 --- a/test/integration/generateAndValidate.ts +++ b/test/integration/generateAndValidate.ts @@ -27,7 +27,7 @@ const bsonDocuments = [{ } }]; -describe.only('Documents -> Generate schema -> Validate Documents against the schema', function() { +describe('Documents -> Generate schema -> Validate Documents against the schema', function() { it('Standard JSON Schema with Relaxed EJSON', async function() { const ajv = new Ajv2020(); // First we get the JSON schema from BSON