Skip to content

Commit

Permalink
feat: add mongodb.dataType to property definition
Browse files Browse the repository at this point in the history
Adds mongodb.dataType to model property definition

Feedback
  • Loading branch information
Hage Yaapa committed Jun 3, 2019
1 parent c6a9020 commit 8d86bdc
Show file tree
Hide file tree
Showing 4 changed files with 323 additions and 79 deletions.
28 changes: 23 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ If you create a MongoDB data source using the data source generator as described

## Creating a MongoDB data source

Use the [Data source generator](http://loopback.io/doc/en/lb3/Data-source-generator.html) to add a MongoDB data source to your application.
Use the [Data source generator](http://loopback.io/doc/en/lb3/Data-source-generator.html) to add a MongoDB data source to your application.
The generator will prompt for the database server hostname, port, and other settings
required to connect to a MongoDB database. It will also run the `npm install` command above for you.

Expand Down Expand Up @@ -64,7 +64,7 @@ If you run a MongoDB with authentification ([Docker's example here](https://gith
`$pop`, `$pullAll`, `$pull`, `$pushAll`, `$push`, and `$bit`. Default is `false`.
- **enableGeoIndexing**: Set to `true` to enable 2dsphere indexing for model properties
of type `GeoPoint`. This allows for indexed ```near``` queries. Default is `false`.
- **lazyConnect**:
- **lazyConnect**:
- Default is `false`.
- If set to `true`, the database instance will not be attached to the datasource and the connection is deferred.
- It will try to establish the connection automatically once users hit the endpoint. If the mongodb server is offline, the app will start, however, the endpoints will not work.
Expand All @@ -74,7 +74,7 @@ of type `GeoPoint`. This allows for indexed ```near``` queries. Default is `fal

### Setting the url property in datasource.json

You can set the `url` property to a connection URL in `datasources.json` to override individual connection parameters such as `host`, `user`, and `password`.
You can set the `url` property to a connection URL in `datasources.json` to override individual connection parameters such as `host`, `user`, and `password`.

Additionally, you can override the global `url` property in environment-specific data source configuration files, for example for production in `datasources.production.json`, and use the individual connection parameters `host`, `user`, `password`, and `port`. To do this, you _must_ set `url` to `false`, null, or “” (empty string).
If you set `url` to `undefined` or remove the `url` property altogether, the override will not work.
Expand All @@ -90,7 +90,7 @@ For example, for production, use `datasources.production.json` as follows (for e
"password": "mypassword",
"name": "mydb",
"user": "me",
"connector": "mongodb"
"connector": "mongodb"
}
```

Expand Down Expand Up @@ -290,11 +290,29 @@ myModelName.find(
{where: {id: {inq: ['59460487e9532ae90c324b59', '59460487e9532ae90c324b5a']}}},
{strictObjectIDCoercion: true},
function(err, result) {
// ...
// ...
}
)
```

## dataType: 'ObjectID'

You can set a model property's `mongodb` property definition `dataType` to "ObjectID" to enforce ObjectID coercion
irrespective of the `strictObjectIDCoercion` setting.

In the following example, the `id` and `xid` will be coerced to `ObjectID` even if `strictObjectIDCoercion` is set to true.

```js
const User = ds.createModel(
'user',
{
id: {type: String, id: true, mongodb: {dataType: 'ObjectID'}},
xid: {type: String, mongodb: {dataType: 'ObjectID'}}
},
{strictObjectIDCoercion: true}
);
```

## Release notes

* 1.1.7 - Do not return MongoDB-specific _id to client API, except if specifically specified in the model definition
176 changes: 119 additions & 57 deletions lib/mongodb.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ const Connector = require('loopback-connector').Connector;
const debug = require('debug')('loopback:connector:mongodb');
const Decimal128 = mongodb.Decimal128;

const ObjectIdValueRegex = /^[0-9a-fA-F]{24}$/;
const ObjectIdTypeRegex = /objectid/i;

exports.ObjectID = ObjectID;
/*!
* Convert the id to be a BSON ObjectID if it is compatible
Expand All @@ -35,7 +38,7 @@ function ObjectID(id) {
// MongoDB's ObjectID constructor accepts number, 12-byte string or 24-byte
// hex string. For LoopBack, we only allow 24-byte hex string, but 12-byte
// string such as 'line-by-line' should be kept as string
if (/^[0-9a-fA-F]{24}$/.test(id)) {
if (ObjectIdValueRegex.test(id)) {
return bson.ObjectID(id);
} else {
return id;
Expand Down Expand Up @@ -180,6 +183,7 @@ function MongoDB(settings, dataSource) {
}

this.dataSource = dataSource;

if (
this.settings.enableOptimisedfindOrCreate === true ||
this.settings.enableOptimisedFindOrCreate === true ||
Expand Down Expand Up @@ -412,11 +416,11 @@ MongoDB.prototype.fromDatabase = function(modelName, data) {
* @param {Object} data The JSON data to convert
*/
MongoDB.prototype.toDatabase = function(modelName, data) {
const modelInstance = this._models[modelName].model;
const props = this._models[modelName].properties;
const modelCtor = this._models[modelName];
const props = modelCtor.properties;

if (this.settings.enableGeoIndexing !== true) {
visitAllProperties(data, modelInstance, coerceDecimalProperty);
visitAllProperties(data, modelCtor, coercePropertyValue);
// Override custom column names
data = this.fromPropertyToDatabaseNames(modelName, data);
return data;
Expand All @@ -433,7 +437,7 @@ MongoDB.prototype.toDatabase = function(modelName, data) {
}
}

visitAllProperties(data, modelInstance, coerceDecimalProperty);
visitAllProperties(data, modelCtor, coercePropertyValue);
// Override custom column names
data = this.fromPropertyToDatabaseNames(modelName, data);
if (debug.enabled) debug('toDatabase data: ', util.inspect(data));
Expand Down Expand Up @@ -528,6 +532,7 @@ MongoDB.prototype.coerceId = function(modelName, id, options) {

// Type conversion for id
const idProp = self.getPropertyDefinition(modelName, idName);

if (idProp && typeof idProp.type === 'function') {
if (!(idValue instanceof idProp.type)) {
idValue = idProp.type(id);
Expand All @@ -537,9 +542,8 @@ MongoDB.prototype.coerceId = function(modelName, id, options) {
}
}

if (self.isObjectIDProperty(modelName, idProp, idValue, options)) {
idValue = ObjectID(idValue);
}
const modelCtor = this._models[modelName];
idValue = coerceToObjectId(modelCtor, idProp, idValue);
}
return idValue;
};
Expand Down Expand Up @@ -841,7 +845,7 @@ MongoDB.prototype.updateOrCreate = function updateOrCreate(
/**
* Replace model instance if it exists or create a new one if it doesn't
*
* @param {String} modelName The name of the model
* @param {String} modelName The model name
* @param {Object} data The model instance data
* @param {Object} options The options object
* @param {Function} [cb] The callback function
Expand Down Expand Up @@ -942,12 +946,12 @@ MongoDB.prototype.buildWhere = function(modelName, where, options) {

const prop = self.getPropertyDefinition(modelName, propName);

const isDecimal = prop &&
prop.mongodb && prop.mongodb.dataType &&
prop.mongodb.dataType.toLowerCase() === 'decimal128';
if (isDecimal) {
cond = Decimal128.fromString(cond);
debug('buildWhere decimal value: %s, constructor name: %s', cond, cond.constructor.name);
if (prop && prop.mongodb && typeof prop.mongodb.dataType === 'string') {
const isDecimal = prop.mongodb.dataType.toLowerCase() === 'decimal128';
if (isDecimal) {
cond = Decimal128.fromString(cond);
debug('buildWhere decimal value: %s, constructor name: %s', cond, cond.constructor.name);
}
}

// Convert property to database column name
Expand All @@ -960,14 +964,17 @@ MongoDB.prototype.buildWhere = function(modelName, where, options) {
spec = Object.keys(cond)[0];
cond = cond[spec];
}

const modelCtor = self._models[modelName];

if (spec) {
if (spec === 'between') {
query[k] = {$gte: cond[0], $lte: cond[1]};
} else if (spec === 'inq') {
cond = [].concat(cond || []);
query[k] = {
$in: cond.map(function(x) {
if (self.isObjectIDProperty(modelName, prop, x, options))
if (isObjectIDProperty(modelCtor, prop, x, options))
return ObjectID(x);
return x;
}),
Expand All @@ -976,7 +983,7 @@ MongoDB.prototype.buildWhere = function(modelName, where, options) {
cond = [].concat(cond || []);
query[k] = {
$nin: cond.map(function(x) {
if (self.isObjectIDProperty(modelName, prop, x, options))
if (isObjectIDProperty(modelCtor, prop, x, options))
return ObjectID(x);
return x;
}),
Expand Down Expand Up @@ -1010,7 +1017,7 @@ MongoDB.prototype.buildWhere = function(modelName, where, options) {
// Null: 10
query[k] = {$type: 10};
} else {
if (self.isObjectIDProperty(modelName, prop, cond, options)) {
if (isObjectIDProperty(modelCtor, prop, cond, options)) {
cond = ObjectID(cond);
}
query[k] = cond;
Expand Down Expand Up @@ -1217,8 +1224,6 @@ MongoDB.prototype.getDatabaseColumnName = function(model, propName) {

const prop = model.properties[propName] || {};

// console.log('getDatabaseColumnName', propName, prop);

// Try mongo overrides
if (prop.mongodb) {
propName =
Expand All @@ -1234,8 +1239,6 @@ MongoDB.prototype.getDatabaseColumnName = function(model, propName) {
propName = prop.columnName || prop.column || propName;
}

// Done
// console.log('->', propName);
return propName;
};

Expand Down Expand Up @@ -1870,30 +1873,72 @@ MongoDB.prototype.ping = function(cb) {
}
};

// Case insensitive check if a string looks like "ObjectID"
function typeIsObjectId(input) {
if (!input) return false;
return typeof input === 'string' && input.match(ObjectIdTypeRegex);
}

// Determine if a property must be stored as ObjectID
function isStoredAsObjectID(propDef) {
if (!propDef) return false;

if (propDef.mongodb) {
if (ObjectIdTypeRegex.test(propDef.mongodb.dataType)) return true;
} else if (propDef.type) {
if (typeof propDef.type === 'string' && typeIsObjectId(propDef.type)) return true;
else if (Array.isArray(propDef.type)) {
if (propDef.type[0] === ObjectID || typeIsObjectId(propDef.type[0])) {
return true;
}
}
}
return false;
}

// Determine if strictObjectIDCoercion should be enabled
function isStrictObjectIDCoercionEnabled(modelCtor, options) {
const settings = modelCtor.settings;
return (settings && settings.strictObjectIDCoercion) ||
(modelCtor.model && modelCtor.model.getConnector().settings.strictObjectIDCoercion) ||
options &&
options.strictObjectIDCoercion;
}

// Tries to coerce a property into ObjectID after checking multiple conditions
function coerceToObjectId(modelCtor, propDef, propValue) {
if (isStoredAsObjectID(propDef)) {
if (isObjectIDProperty(modelCtor, propDef, propValue)) {
return ObjectID(propValue);
} else {
throw new Error(`${propValue} is not an ObjectID string`);
}
} else if (isStrictObjectIDCoercionEnabled(modelCtor)) {
if (isObjectIDProperty(modelCtor, propDef, propValue)) {
return ObjectID(propValue);
}
} else if (ObjectIdValueRegex.test(propValue)) {
return ObjectID(propValue);
}
return propValue;
}

/**
* Check whether the property is an ObjectID (or Array thereof)
*
*/
MongoDB.prototype.isObjectIDProperty = function(modelName, prop, value, options) {
if (
prop &&
(prop.type === ObjectID ||
(Array.isArray(prop.type) && prop.type[0] === ObjectID))
) {
function isObjectIDProperty(modelCtor, propDef, value, options) {
if (!propDef) return false;

if (typeof value === 'string' && value.match(ObjectIdValueRegex)) {
if (isStoredAsObjectID(propDef)) return true;
else return !isStrictObjectIDCoercionEnabled(modelCtor, options);
} else if (value instanceof ObjectID) {
return true;
} else if ('string' === typeof value) {
const settings = this._models[modelName] && this._models[modelName].settings;
options = options || {};
const strict =
(settings && settings.strictObjectIDCoercion) ||
this.settings.strictObjectIDCoercion ||
options.strictObjectIDCoercion;
if (strict) return false; // unless explicitly typed, don't coerce
return /^[0-9a-fA-F]{24}$/.test(value);
} else {
return false;
}
};
}

function sanitizeFilter(filter, options) {
options = Object.assign({}, options);
Expand Down Expand Up @@ -2001,15 +2046,14 @@ function optimizedFindOrCreate(modelName, filter, data, options, callback) {
}

/**
*
* @param {*} data Plain Data Object for the matching property definition(s)
* @param {*} modelCtorOrDef Model constructor or definition
* @param {*} modelCtor Model constructor
* @param {*} visitor A callback function which takes a property value and
* definition to apply custom property coercion
*/
function visitAllProperties(data, modelCtorOrDef, visitor) {
function visitAllProperties(data, modelCtor, visitor) {
if (data === null || data === undefined) return;
const modelProps = modelCtorOrDef.properties ? modelCtorOrDef.properties : modelCtorOrDef.definition.properties;
const modelProps = modelCtor.properties ? modelCtor.properties : modelCtor.definition.properties;
const allProps = new Set(Object.keys(data).concat(Object.keys(modelProps)));
for (const p of allProps) {
const value = data[p];
Expand All @@ -2024,28 +2068,47 @@ function visitAllProperties(data, modelCtorOrDef, visitor) {
visitAllProperties(value, def.type.definition, visitor);
}
} else {
visitor(value, def, (newValue) => { data[p] = newValue; });
visitor(modelCtor, value, def, (newValue) => { data[p] = newValue; });
}
continue;
}
}

/**
*
* @param {*} propValue Property value to coerce into a Decimal128 value
* @param {*} propDef Property definition to check if property is MongoDB
* Decimal128 type
* @param {*} modelCtor Model constructor
* @param {*} propValue Property value to coerce into special types supported by the connector
* @param {*} propDef Property definition to check if property is for MongoDB
*/
function coerceDecimalProperty(propValue, propDef, setValue) {
let updatedValue;
if (hasDataType('decimal128', propDef)) {
if (Array.isArray(propValue)) {
updatedValue = propValue.map(val => Decimal128.fromString(val));
return setValue(updatedValue);
} else {
updatedValue = Decimal128.fromString(propValue);
return setValue(updatedValue);
function coercePropertyValue(modelCtor, propValue, propDef, setValue) {
let coercedValue;
// Process only mongo-specific data types
if (propDef && propDef.mongodb && propDef.mongodb.dataType) {
const dataType = propDef.mongodb.dataType;
if (typeof dataType === 'string') {
if (hasDataType('decimal128', propDef)) {
if (Array.isArray(propValue)) {
coercedValue = propValue.map(val => Decimal128.fromString(val));
return setValue(coercedValue);
} else {
coercedValue = Decimal128.fromString(propValue);
return setValue(coercedValue);
}
} else if (typeIsObjectId(dataType)) {
if (isObjectIDProperty(modelCtor, propDef, propValue)) {
coercedValue = ObjectID(propValue);
return setValue(coercedValue);
} else {
throw new Error(`${propValue} is not an ObjectID string`);
}
}
} else if (dataType instanceof ObjectID) {
coercedValue = ObjectID(propValue);
return setValue(coercedValue);
}
} else {
// Object ID coercibility depends on multiple factors, let coerceToObjectId() handle it
propValue = coerceToObjectId(modelCtor, propDef, propValue);
setValue(propValue);
}
}

Expand All @@ -2072,4 +2135,3 @@ function hasDataType(dataType, propertyDef) {
propertyDef.mongodb.dataType &&
propertyDef.mongodb.dataType.toLowerCase() === dataType.toLowerCase();
}

Loading

0 comments on commit 8d86bdc

Please sign in to comment.