Skip to content
This repository has been archived by the owner on Sep 2, 2022. It is now read-only.

Commit

Permalink
Added proper support for Mongo BSON data types.
Browse files Browse the repository at this point in the history
Added useful errors for type conflicts.
  • Loading branch information
ejoebstl authored and timsuchanek committed Dec 18, 2018
1 parent ea7c5a1 commit 65e99ca
Show file tree
Hide file tree
Showing 7 changed files with 124 additions and 12 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import * as BSON from 'bson'

// Client doc: https://mongodb.github.io/node-mongodb-native/api-bson-generated/bson.html
// Database doc: https://docs.mongodb.com/manual/reference/bson-types/
export const scalars = [{
_id: 0,
double: new BSON.Double(0.5),
string: 'string',
object: { test: 'test' },
array: [1, 2, 3],
binary: new BSON.Binary(new Buffer(5)),
objectId: new BSON.ObjectID(),
boolean: true,
date: new Date(),
null: null,
regex: new BSON.BSONRegExp('\\w', 'i'),
javascript: new BSON.Code('alert("hello")'),
int32: new BSON.Int32(10),
timestamp: new BSON.Timestamp(10, 10),
int64: new BSON.Long(10, 10),
decimal128: new BSON.Decimal128(new Buffer(4))
}]

export const schemaString = `type scalars {
# Type Int is currently not supported for id fields.
_id: Int! @id
array: [Int!]!
# Field type not supported: Binary
# binary: <Unknown>
boolean: Boolean
date: DateTime
# Field type not supported: Decimal128
# decimal128: <Unknown>
double: Float
int32: Int
int64: Int
# Field type not supported: Code
# javascript: <Unknown>
# Field type not supported: null
# null: <Unknown>
object: scalarsObject
objectId: ID
# Field type not supported: RegExp
# regex: <Unknown>
string: String
# Field type not supported: Timestamp
# timestamp: <Unknown>
}
type scalarsObject @embedded {
test: String
}`
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { collections, schemaString } from '../data/webshop'

const env = new MongoTestEnvironment()

describe.skip('Mongo Model Introspector, end to end', () => {
describe('Mongo Model Introspector, end to end', () => {
beforeAll(async () => await env.connect())
afterAll(async () => await env.disconnect())
afterEach(async () => await env.clear())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {

const env = new MongoTestEnvironment()

describe.skip('Mongo Model Introspector', () => {
describe('Mongo Model Introspector', () => {
beforeAll(async () => await env.connect())
afterAll(async () => await env.disconnect())
afterEach(async () => await env.clear())
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { MongoConnector } from '../../../databases/document/mongo/mongoConnector'
import { MongoTestEnvironment } from '../../../test-helpers/mongoTestEnvironment'
import { scalars, schemaString } from '../data/mongoTypes'

const env = new MongoTestEnvironment()

describe('Mongo Model Introspector, end to end', () => {
beforeAll(async () => await env.connect())
afterAll(async () => await env.disconnect())
afterEach(async () => await env.clear())

it('Scalar Types', async () => {
await env.createCollections({ scalars })

const connector = new MongoConnector(env.getClient())
const introspection = await connector.introspect(env.schemaName)
const schema = await introspection.renderToDatamodelString()

expect(schema).toEqual(schemaString)
}, 10000)
})
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export abstract class DocumentConnector<InternalCollectionType> implements IDocu
// Special case: We treat int as a case of float, if needed.
type.type = TypeIdentifiers.float
} else if(type.type !== itemType.type) {
throw new UnsupportedArrayTypeError('Mixed arrays are not supported.', `Array of ${type} and ${itemType.type}`)
throw new UnsupportedArrayTypeError('Mixed arrays are not supported.', `[${type.type} | ${itemType.type}]`)
}
}

Expand Down Expand Up @@ -121,7 +121,14 @@ export abstract class DocumentConnector<InternalCollectionType> implements IDocu
case "boolean": return { type: TypeIdentifiers.boolean, isArray: false, isRelationCandidate: false }
// Base case: String types might identify relations.
case "string": return { type: TypeIdentifiers.string, isArray: false, isRelationCandidate: true }
case "object": return { type: ObjectTypeIdentifier, isArray: false, isRelationCandidate: false }
case "object":
if(value instanceof Date) {
return { type: TypeIdentifiers.dateTime, isArray: false, isRelationCandidate: false }
} else if (value === null) {
throw new UnsupportedTypeError('Received an unsupported type: ', 'null')
} else {
return { type: ObjectTypeIdentifier, isArray: false, isRelationCandidate: false }
}
default: break
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ export class ModelMerger {

// Check for inconsistent data type
let typeCandidates = this.summarizeTypeList([...info.types])
let invalidTypes = info.invalidTypes

if(typeCandidates.length > 1) {
comments.push({
Expand All @@ -191,13 +192,27 @@ export class ModelMerger {
}

// Check for missing data type
// If we have a type error, let the type be ModelSampler.ErrorType,
// so we have a constant to check for.
if(typeCandidates.length === 1) {
type = typeCandidates[0]
} else {
} else if(invalidTypes.length === 0) {
// No type info at all.
comments.push({
isError: true,
text: 'No type information found for field.'
})
} else if(invalidTypes.length === 1) {
// No conflict, but an invalid type
comments.push({
isError: true,
text: 'Field type not supported: ' + invalidTypes[0]
})
} else {
comments.push({
isError: true,
text: 'Field type not found due to conflict. Candidates: ' + [...typeCandidates].join(', ')
})
}

// TODO: Abstract away
Expand Down Expand Up @@ -276,15 +291,15 @@ export class ModelMerger {
// On error, register an invalid type.
if(err.name === UnsupportedTypeErrorKey) {
this.initField(name)
this.fields[name].invalidTypes.push(err.invalidType)
this.fields[name].invalidTypes = this.merge( this.fields[name].invalidTypes, err.invalidType)
} else if(err.name == UnsupportedArrayTypeErrorKey) {
this.initField(name)
this.fields[name] = this.mergeField(this.fields[name], {
isArray: true,
type: null,
isRelationCandidate: false
})
this.fields[name].invalidTypes.push(err.invalidType)
this.fields[name].invalidTypes = this.merge( this.fields[name].invalidTypes, err.invalidType)
} else {
throw err
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { IDataIterator, SamplingStrategy, ObjectTypeIdentifier, TypeInfo } from '../documentConnector'
import { IDataIterator, SamplingStrategy, ObjectTypeIdentifier, TypeInfo, UnsupportedTypeError } from '../documentConnector'
import { DocumentConnector } from '../documentConnectorBase'
import { DatabaseType, ISDL } from "prisma-datamodel"
import { DocumentIntrospectionResult } from "../documentIntrospectionResult"
import { MongoClient, Collection, Cursor, AggregationCursor } from 'mongodb'
import { ObjectID } from 'bson'
import * as BSON from 'bson'
import { Data } from '../data'
import { TypeIdentifiers } from '../../../../../prisma-datamodel/dist/datamodel/scalar';

Expand Down Expand Up @@ -95,9 +95,26 @@ export class MongoConnector extends DocumentConnector<Collection<Data>>{
public inferType(value: any): TypeInfo {
const suggestion = super.inferType(value)

if(suggestion.type === ObjectTypeIdentifier && value instanceof ObjectID) {
suggestion.type = TypeIdentifiers.id
suggestion.isRelationCandidate = true
if(suggestion.type === ObjectTypeIdentifier) {
// Special BSON types. DateTime is JS DateTime and handled by base class.
if(value instanceof BSON.ObjectID) {
suggestion.type = TypeIdentifiers.id
suggestion.isRelationCandidate = true
} else if (value instanceof BSON.Binary) {
throw new UnsupportedTypeError('Type not supported', 'Binary')
} else if (value instanceof BSON.BSONRegExp || value instanceof RegExp) {
throw new UnsupportedTypeError('Type not supported', 'RegExp')
} else if (value instanceof BSON.Code) {
throw new UnsupportedTypeError('Type not supported', 'Code')
} else if (value instanceof BSON.Int32) {
suggestion.type = TypeIdentifiers.integer
} else if (value instanceof BSON.Timestamp) {
throw new UnsupportedTypeError('Type not supported', 'Timestamp')
} else if (value instanceof BSON.Long) {
suggestion.type = TypeIdentifiers.long
} else if (value instanceof BSON.Decimal128) {
throw new UnsupportedTypeError('Type not supported', 'Decimal128')
}
}

return suggestion
Expand Down

0 comments on commit 65e99ca

Please sign in to comment.