Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor date, timestamp and datetime data types handling #257

Merged
merged 11 commits into from
Apr 18, 2017
53 changes: 40 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ Edit `datasources.json` to add any other additional properties that you require.
<td>Enable this option to deal with big numbers (BIGINT and DECIMAL columns) in the database. Default is false.</td>
</tr>
<tr>
<td>timeZone</td>
<td>timezone</td>
<td>String</td>
<td>The timezone used to store local dates. Default is ‘local’.</td>
</tr>
Expand Down Expand Up @@ -280,6 +280,45 @@ Example:
}
```

### Date types

For TIMESTAMP and DATE types, use the `dateType` option to specify custom type. By default it is DATETIME.

Example:

```javascript
{ startTime :
{ type: Date,
dataType: 'timestamp'
}
}
```

**Note:** When quering a `DATE` type, please be aware that values sent to the server via REST API call will be converted to a Date object using the server timezone. Then, only `YYYY-MM-DD` part of the date will be used for the SQL query.

For example, if the client and the server is in GMT+2 and GMT -2 timezone respectively. Performing the following operation at `02:00 on 2016/11/22` from the client side:

```javascript
var products = Product.find({where:{expired:new Date(2016,11,22)}});
```

will result in the REST URL to look like: `/api/Products/?filter={"where":{"expired":"2016-12-21T22:00:00Z"}}` and the SQL will be like this:

```SQL
SELECT * FROM Product WHERE expired = '2016-12-21'
```
which is not correct.

**Solution:** The workaround to avoid such edge case boundaries with timezones is to use the `DATE` type field as a **_string_** type in the LoopBack model definition.

```javascript
{ birthday :
{ type: String,
dataType: 'date'
}
}
```

### Other types

Convert String / DataSource.Text / DataSource.JSON to the following MySQL types:
Expand Down Expand Up @@ -312,18 +351,6 @@ Example: 
}
```

Convert JSON Date types to  datetime or timestamp

Example: 

```javascript
{ startTime :
{ type: Date,
dataType: 'timestamp'
}
}
```

### Enum

Enums are special. Create an Enum using Enum factory:
Expand Down
60 changes: 51 additions & 9 deletions lib/mysql.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,9 @@ function generateOptions(settings) {
charset: s.collation.toUpperCase(), // Correct by docs despite seeming odd.
supportBigNumbers: s.supportBigNumbers,
connectionLimit: s.connectionLimit,
//prevent mysqljs from converting DATE, DATETIME and TIMESTAMP types
//to javascript Date object
dateStrings: true,
};

// Don't configure the DB if the pool can be used for multiple DBs
Expand Down Expand Up @@ -307,17 +310,40 @@ MySQL.prototype.updateOrCreate = function(model, data, options, cb) {
this._modifyOrCreate(model, data, options, fields, cb);
};

function dateToMysql(val) {
return val.getUTCFullYear() + '-' +
fillZeros(val.getUTCMonth() + 1) + '-' +
fillZeros(val.getUTCDate()) + ' ' +
fillZeros(val.getUTCHours()) + ':' +
fillZeros(val.getUTCMinutes()) + ':' +
fillZeros(val.getUTCSeconds());
function dateToMysql(dt, tz) {
if (!tz || tz == 'local') {
return dt.getFullYear() + '-' +
fillZeros(dt.getMonth() + 1) + '-' +
fillZeros(dt.getDate()) + ' ' +
fillZeros(dt.getHours()) + ':' +
fillZeros(dt.getMinutes()) + ':' +
fillZeros(dt.getSeconds());
} else {
tz = convertTimezone(tz);

if (tz !== false && tz !== 0) dt.setTime(dt.getTime() + (tz * 60000));

return dt.getUTCFullYear() + '-' +
fillZeros(dt.getUTCMonth() + 1) + '-' +
fillZeros(dt.getUTCDate()) + ' ' +
fillZeros(dt.getUTCHours()) + ':' +
fillZeros(dt.getUTCMinutes()) + ':' +
fillZeros(dt.getUTCSeconds());
}

function fillZeros(v) {
return v < 10 ? '0' + v : v;
}

function convertTimezone(tz) {
if (tz === 'Z') {
return 0;
}

var m = tz.match(/([\+\-\s])(\d\d):?(\d\d)?/);
if (m) return (m[1] == '-' ? -1 : 1) * (parseInt(m[2], 10) + ((m[3] ? parseInt(m[3], 10) : 0) / 60)) * 60;
return false;
}
}

MySQL.prototype.getInsertedId = function(model, info) {
Expand Down Expand Up @@ -356,6 +382,13 @@ MySQL.prototype.toColumnValue = function(prop, val) {
if (!val.toUTCString) {
val = new Date(val);
}

if (prop.dataType == 'date') {
return dateToMysql(val).substring(0, 10);
} else if (prop.dataType == 'timestamp' || (prop.mysql && prop.mysql.dataType == 'timestamp')) {
var tz = this.client.config.connectionConfig.timezone;
return dateToMysql(val, tz);
}
return dateToMysql(val);
}
if (prop.type === Boolean) {
Expand Down Expand Up @@ -415,10 +448,19 @@ MySQL.prototype.fromColumnValue = function(prop, val) {
// MySQL allows, unless NO_ZERO_DATE is set, dummy date/time entries
// new Date() will return Invalid Date for those, so we need to handle
// those separate.
if (val == '0000-00-00 00:00:00') {
if (!val || val == '0000-00-00 00:00:00' || val == '0000-00-00') {
val = null;
} else {
val = new Date(val.toString().replace(/GMT.*$/, 'GMT'));
var dateString = val;
var tz = this.client.config.connectionConfig.timezone;
if (prop.dataType == 'date') {
dateString += ' 00:00:00';
}
//if datatype is timestamp and zimezone is not local - convert to proper timezone
if (tz !== 'local' && (prop.dataType == 'timestamp' || (prop.mysql && prop.mysql.dataType == 'timestamp'))) {
dateString += ' ' + tz;
}
return new Date(dateString);
}
break;
case 'Boolean':
Expand Down
Loading