diff --git a/package.json b/package.json index d7967fc..e6c4a66 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ ".esm-wrapper.mjs" ], "scripts": { - "test": "nyc mocha --timeout 5000 --colors -r ts-node/register test/*.ts", + "test": "nyc mocha --timeout 5000 --colors -r ts-node/register src/**/*.test.ts", "test-example-parse-from-file": "ts-node examples/parse-from-file.ts", "test-example-parse-schema": "ts-node examples/parse-schema.ts", "test-time": "ts-node ./test/time-testing.ts", diff --git a/src/schema-convertors/bsontypes.ts b/src/schema-convertors/bsontypes.ts new file mode 100644 index 0000000..e43f950 --- /dev/null +++ b/src/schema-convertors/bsontypes.ts @@ -0,0 +1,37 @@ +// function parseType(type: SchemaType, signal?: AbortSignal): StandardJSONSchema { +// switch (type.bsonType) { +// case 'Array': return { +// type: 'array', +// items: parseTypes((type as ArraySchemaType).types) +// }; +// case 'Binary': return { +// type: 'string' +// // contentEncoding: // TODO: can we get this? +// }; +// case 'Boolean': return { +// type: 'boolean' +// }; +// case 'Document': return { +// type: 'object', +// ...parseFields((type as DocumentSchemaType).fields, signal) +// }; +// case 'Double': return { +// type: 'number' +// }; +// case 'Null': return { +// type: 'null' +// }; +// case 'ObjectId': return { +// type: 'string', +// contentEncoding: 'base64' // TODO: confirm +// }; +// case 'String': return { +// type: 'string' +// }; +// case 'Timestamp': return { +// type: 'string' +// // TODO +// }; +// default: throw new Error('Type unknown ' + type.bsonType); // TODO: unknown + telemetry? +// } +// } \ No newline at end of file diff --git a/src/schema-convertors.ts b/src/schema-convertors/index.ts similarity index 60% rename from src/schema-convertors.ts rename to src/schema-convertors/index.ts index 435e991..d385596 100644 --- a/src/schema-convertors.ts +++ b/src/schema-convertors/index.ts @@ -1,14 +1,6 @@ -import { Schema as InternalSchema } from './schema-analyzer'; -import { ExtendedJSONSchema, MongoDBJSONSchema, StandardJSONSchema } from './types'; - -function internalSchemaToStandard( - internalSchema: InternalSchema, - options: { - signal?: AbortSignal -}): StandardJSONSchema { - // TODO: COMPASS-8700 - return {}; -} +import internalSchemaToStandard from '../internalToStandard'; +import { Schema as InternalSchema } from '../schema-analyzer'; +import { ExtendedJSONSchema, MongoDBJSONSchema } from '../types'; function internalSchemaToMongoDB( internalSchema: InternalSchema, diff --git a/src/schema-convertors/internalToMongodb.test.ts b/src/schema-convertors/internalToMongodb.test.ts new file mode 100644 index 0000000..dbd9731 --- /dev/null +++ b/src/schema-convertors/internalToMongodb.test.ts @@ -0,0 +1,206 @@ +import assert from 'assert'; +import internalSchemaToStandard from './internalToMongodb'; + +describe.only('internalSchemaToStandard', function() { + it('converts a document/object', 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 = internalSchemaToStandard(internal); + assert.deepStrictEqual(standard, { + $jsonSchema: { + bsonType: 'object', + required: ['author'], + properties: { + author: { + bsonType: 'object', + required: ['name', 'rating'], + properties: { + name: { + bsonType: 'string' + }, + rating: { + bsonType: 'double' + } + } + } + } + } + }); + }); + + it('converts an array', 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 = internalSchemaToStandard(internal); + assert.deepStrictEqual(standard, { + $jsonSchema: { + bsonType: 'object', + required: [], + properties: { + genres: { + bsonType: 'array', + items: { + bsonType: 'string' + } + } + } + } + }); + }); +}); diff --git a/src/schema-convertors/internalToMongodb.ts b/src/schema-convertors/internalToMongodb.ts new file mode 100644 index 0000000..fae2cb4 --- /dev/null +++ b/src/schema-convertors/internalToMongodb.ts @@ -0,0 +1,65 @@ +import { ArraySchemaType, DocumentSchemaType, Schema as InternalSchema, SchemaType } from '../schema-analyzer'; +import { StandardJSONSchema } from '../types'; + +const internalTypeToBsonType = (type: string) => type === 'Document' ? 'object' : type.toLowerCase(); + +function parseType(type: SchemaType, signal?: AbortSignal): StandardJSONSchema { + if (signal?.aborted) throw new Error('Operation aborted'); + const schema: StandardJSONSchema = { + bsonType: internalTypeToBsonType(type.bsonType) + }; + switch (type.bsonType) { + case 'Array': + schema.items = parseTypes((type as ArraySchemaType).types); + break; + case 'Document': + Object.assign(schema, + parseFields((type as DocumentSchemaType).fields, signal) + ); + break; + } + + return schema; +} + +function parseTypes(types: SchemaType[], signal?: AbortSignal): StandardJSONSchema { + if (signal?.aborted) throw new Error('Operation aborted'); + const definedTypes = types.filter(type => type.bsonType.toLowerCase() !== 'undefined'); + const isSingleType = definedTypes.length === 1; + if (isSingleType) { + return parseType(definedTypes[0], signal); + } + // TODO: array of types for simple types + return { + anyOf: definedTypes.map(type => parseType(type, signal)) + }; +} + +function parseFields(fields: DocumentSchemaType['fields'], signal?: AbortSignal): { + required: StandardJSONSchema['required'], + properties: StandardJSONSchema['properties'], +} { + const required = []; + const properties: StandardJSONSchema['properties'] = {}; + for (const field of fields) { + if (signal?.aborted) throw new Error('Operation aborted'); + if (field.probability === 1) required.push(field.name); + properties[field.name] = parseTypes(field.types, signal); + } + + return { required, properties }; +} + +export default function internalSchemaToMongodb( + internalSchema: InternalSchema, + options: { + signal?: AbortSignal +} = {}): StandardJSONSchema { + const schema: StandardJSONSchema = { + $jsonSchema: { + bsonType: 'object', + ...parseFields(internalSchema.fields, options.signal) + } + }; + return schema; +}